diff --git a/.env.example b/.env.example index 12d242e..07aac9c 100644 --- a/.env.example +++ b/.env.example @@ -16,9 +16,10 @@ GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google SMTP_TRANSPORT=smtps://toto:tata@relay MAIL_AUTHOR='"Chromacase" ' 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 diff --git a/assets/create_melodies.sh b/assets/create_melodies.sh new file mode 100755 index 0000000..a8013d3 --- /dev/null +++ b/assets/create_melodies.sh @@ -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 + diff --git a/assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3 b/assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3 new file mode 100644 index 0000000..7a21267 Binary files /dev/null and b/assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3 differ diff --git a/assets/musics/Bach Polonaise in g Minor (BWV Anh. 119)/melody.mp3 b/assets/musics/Bach Polonaise in g Minor (BWV Anh. 119)/melody.mp3 new file mode 100644 index 0000000..6c5813a Binary files /dev/null and b/assets/musics/Bach Polonaise in g Minor (BWV Anh. 119)/melody.mp3 differ diff --git a/assets/musics/Beethoven Symphony 7 2nd Movement (Allegretto) Simple Piano arrangement/melody.mp3 b/assets/musics/Beethoven Symphony 7 2nd Movement (Allegretto) Simple Piano arrangement/melody.mp3 new file mode 100644 index 0000000..075760c Binary files /dev/null and b/assets/musics/Beethoven Symphony 7 2nd Movement (Allegretto) Simple Piano arrangement/melody.mp3 differ diff --git a/assets/musics/Canon in D (easy)/melody.mp3 b/assets/musics/Canon in D (easy)/melody.mp3 new file mode 100644 index 0000000..4dcec96 Binary files /dev/null and b/assets/musics/Canon in D (easy)/melody.mp3 differ diff --git a/assets/musics/El pequeño tamborilero (The little drummer boy)/melody.mp3 b/assets/musics/El pequeño tamborilero (The little drummer boy)/melody.mp3 new file mode 100644 index 0000000..f36ad8d Binary files /dev/null and b/assets/musics/El pequeño tamborilero (The little drummer boy)/melody.mp3 differ diff --git a/assets/musics/Erik Satie - Gnossienne No.1. {Professional production score.}/melody.mp3 b/assets/musics/Erik Satie - Gnossienne No.1. {Professional production score.}/melody.mp3 new file mode 100644 index 0000000..6f26158 Binary files /dev/null and b/assets/musics/Erik Satie - Gnossienne No.1. {Professional production score.}/melody.mp3 differ diff --git a/assets/musics/French National Anthem La Marseillaise/melody.mp3 b/assets/musics/French National Anthem La Marseillaise/melody.mp3 new file mode 100644 index 0000000..0872db1 Binary files /dev/null and b/assets/musics/French National Anthem La Marseillaise/melody.mp3 differ diff --git a/assets/musics/German National Anthem Das Lied der Deutschen/melody.mp3 b/assets/musics/German National Anthem Das Lied der Deutschen/melody.mp3 new file mode 100644 index 0000000..56eb5b4 Binary files /dev/null and b/assets/musics/German National Anthem Das Lied der Deutschen/melody.mp3 differ diff --git a/assets/musics/Jesus Alegria dos Homens/melody.mp3 b/assets/musics/Jesus Alegria dos Homens/melody.mp3 new file mode 100644 index 0000000..725f522 Binary files /dev/null and b/assets/musics/Jesus Alegria dos Homens/melody.mp3 differ diff --git a/assets/musics/Liebestraum (easy)/melody.mp3 b/assets/musics/Liebestraum (easy)/melody.mp3 new file mode 100644 index 0000000..dbe6095 Binary files /dev/null and b/assets/musics/Liebestraum (easy)/melody.mp3 differ diff --git a/assets/musics/Mary, Did You Know/melody.mp3 b/assets/musics/Mary, Did You Know/melody.mp3 new file mode 100644 index 0000000..16eb386 Binary files /dev/null and b/assets/musics/Mary, Did You Know/melody.mp3 differ diff --git a/assets/musics/SCORO_TEST/melody.mp3 b/assets/musics/SCORO_TEST/melody.mp3 new file mode 100644 index 0000000..cb2183e Binary files /dev/null and b/assets/musics/SCORO_TEST/melody.mp3 differ diff --git a/assets/musics/Sarabande - William Gillock/melody.mp3 b/assets/musics/Sarabande - William Gillock/melody.mp3 new file mode 100644 index 0000000..34fab2e Binary files /dev/null and b/assets/musics/Sarabande - William Gillock/melody.mp3 differ diff --git a/assets/musics/Short/Short.mid b/assets/musics/Short/Short.mid deleted file mode 100644 index 7896e4e..0000000 Binary files a/assets/musics/Short/Short.mid and /dev/null differ diff --git a/assets/musics/Short/melody.mp3 b/assets/musics/Short/melody.mp3 new file mode 100644 index 0000000..f595e2b Binary files /dev/null and b/assets/musics/Short/melody.mp3 differ diff --git a/assets/musics/Silent Night/melody.mp3 b/assets/musics/Silent Night/melody.mp3 new file mode 100644 index 0000000..8c5d003 Binary files /dev/null and b/assets/musics/Silent Night/melody.mp3 differ diff --git a/assets/musics/Tango La Cumparsita - Piano Solo (Tutorial Parte B)/melody.mp3 b/assets/musics/Tango La Cumparsita - Piano Solo (Tutorial Parte B)/melody.mp3 new file mode 100644 index 0000000..37112b0 Binary files /dev/null and b/assets/musics/Tango La Cumparsita - Piano Solo (Tutorial Parte B)/melody.mp3 differ diff --git a/assets/musics/Twinkle Twinkle Little Star/melody.mp3 b/assets/musics/Twinkle Twinkle Little Star/melody.mp3 new file mode 100644 index 0000000..0bee31f Binary files /dev/null and b/assets/musics/Twinkle Twinkle Little Star/melody.mp3 differ diff --git a/assets/musics/На сопках Маньчжурии (On the Hills of Manchuria)/melody.mp3 b/assets/musics/На сопках Маньчжурии (On the Hills of Manchuria)/melody.mp3 new file mode 100644 index 0000000..fb989e5 Binary files /dev/null and b/assets/musics/На сопках Маньчжурии (On the Hills of Manchuria)/melody.mp3 differ diff --git a/back/src/song/song.controller.ts b/back/src/song/song.controller.ts index b10206e..442d67c 100644 --- a/back/src/song/song.controller.ts +++ b/back/src/song/song.controller.ts @@ -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: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c535ba9..52b7e90 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -67,6 +67,7 @@ services: - NGINX_PORT=4567 ports: - "19006:19006" + - "8081:8081" volumes: - ./front:/app depends_on: diff --git a/front/.expo-shared/assets.json b/front/.expo-shared/assets.json deleted file mode 100644 index 1e6decf..0000000 --- a/front/.expo-shared/assets.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, - "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true -} diff --git a/front/API.ts b/front/API.ts index bb4dc41..64c2e6d 100644 --- a/front/API.ts +++ b/front/API.ts @@ -721,5 +721,9 @@ export default class API { ); }, }; + } + + public static getPartitionMelodyUrl(songId: number): string { + return `${API.baseUrl}/song/${songId}/assets/melody`; } } diff --git a/front/Dockerfile.dev b/front/Dockerfile.dev index 732b370..5646a7f 100644 --- a/front/Dockerfile.dev +++ b/front/Dockerfile.dev @@ -4,4 +4,4 @@ WORKDIR /app RUN yarn global add npx expo-cli ENV DEVAPIURL http://back:3000 -CMD npx expo start --web \ No newline at end of file +CMD npx expo start --web diff --git a/front/Navigation.tsx b/front/Navigation.tsx index f3cb08f..f318002 100644 --- a/front/Navigation.tsx +++ b/front/Navigation.tsx @@ -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(); +const Tab = createCustomNavigator(); + +const Tabs = () => { + return ( + + {Object.entries(tabRoutes).map(([name, route], routeIndex) => ( + + ))} + + ); +}; // 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 = { - component: (arg: RouteProps) => JSX.Element | (() => JSX.Element); + component: ComponentType; options: object; + link?: string; }; -type OmitOrUndefined = T extends undefined ? T : Omit; +// if the component has no props, ComponentProps return unknown so we remove those +type RemoveNonObjects = [T] extends [{}] ? T : undefined; type RouteParams> = { - [RouteName in keyof Routes]: OmitOrUndefined< - Parameters[0], - keyof NativeStackScreenProps<{}> - >; + [RouteName in keyof Routes]: RemoveNonObjects>; }; -type PrivateRoutesParams = RouteParams>; -type PublicRoutesParams = RouteParams>; -type AppRouteParams = PrivateRoutesParams & PublicRoutesParams; +type PrivateRoutesParams = RouteParams; +type PublicRoutesParams = RouteParams; +type TabsRoutesParams = RouteParams; +type AppRouteParams = Omit & { + Tabs: { screen: keyof TabsRoutesParams }; +} & PublicRoutesParams & + TabsRoutesParams & { Oops: undefined }; -const Stack = createNativeStackNavigator(); +const RouteToScreen = (Component: Route['component']) => + function Route(props: NativeStackScreenProps) { + const colorScheme = useColorScheme(); + const insets = useSafeAreaInsets(); -const RouteToScreen = - (component: Route['component']) => - // eslint-disable-next-line react/display-name - (props: NativeStackScreenProps) => ( - <> - {component({ ...props.route.params, route: props.route } as Parameters< - Route['component'] - >[0])} - - ); + return ( + + + + ); + }; const routesToScreens = (routes: Partial>) => Object.entries(routes).map(([name, route], routeIndex) => ( )); -const routesToLinkingConfig = ( - routes: Partial< - Record string> }> - > -) => { +type RouteDescription = Record< + string, + { link?: string; stringify?: Record 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; 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 ; } + const routes = authStatus == 'authed' ? { ...protectedRoutes } : publicRoutes; return ( } theme={colorScheme == 'light' ? DefaultTheme : DarkTheme} > - + {authStatus == 'error' ? ( <> { userProfile.refetch()} /> ))} /> - {routesToScreens(publicRoutes())} + {routesToScreens(publicRoutes)} ) : ( routesToScreens(routes) @@ -282,6 +325,4 @@ export const Router = () => { ); }; -export type RouteProps = T & Pick, 'route'>; - -export const useNavigation = () => navigationHook>(); +export const useNavigation = () => navigationHook>(); diff --git a/front/assets/piano/a0.mp3 b/front/assets/piano/a0.mp3 deleted file mode 100644 index 7c34e27..0000000 Binary files a/front/assets/piano/a0.mp3 and /dev/null differ diff --git a/front/assets/piano/a1.mp3 b/front/assets/piano/a1.mp3 deleted file mode 100644 index f9cd81f..0000000 Binary files a/front/assets/piano/a1.mp3 and /dev/null differ diff --git a/front/assets/piano/a2.mp3 b/front/assets/piano/a2.mp3 deleted file mode 100644 index b4a5812..0000000 Binary files a/front/assets/piano/a2.mp3 and /dev/null differ diff --git a/front/assets/piano/a3.mp3 b/front/assets/piano/a3.mp3 deleted file mode 100644 index 3915e46..0000000 Binary files a/front/assets/piano/a3.mp3 and /dev/null differ diff --git a/front/assets/piano/a4.mp3 b/front/assets/piano/a4.mp3 deleted file mode 100644 index 5b078fd..0000000 Binary files a/front/assets/piano/a4.mp3 and /dev/null differ diff --git a/front/assets/piano/a5.mp3 b/front/assets/piano/a5.mp3 deleted file mode 100644 index 33a9fc8..0000000 Binary files a/front/assets/piano/a5.mp3 and /dev/null differ diff --git a/front/assets/piano/a6.mp3 b/front/assets/piano/a6.mp3 deleted file mode 100644 index 0e8497b..0000000 Binary files a/front/assets/piano/a6.mp3 and /dev/null differ diff --git a/front/assets/piano/a7.mp3 b/front/assets/piano/a7.mp3 deleted file mode 100644 index ee44c2f..0000000 Binary files a/front/assets/piano/a7.mp3 and /dev/null differ diff --git a/front/assets/piano/ab1.mp3 b/front/assets/piano/ab1.mp3 deleted file mode 100644 index b674389..0000000 Binary files a/front/assets/piano/ab1.mp3 and /dev/null differ diff --git a/front/assets/piano/ab2.mp3 b/front/assets/piano/ab2.mp3 deleted file mode 100644 index b44b656..0000000 Binary files a/front/assets/piano/ab2.mp3 and /dev/null differ diff --git a/front/assets/piano/ab3.mp3 b/front/assets/piano/ab3.mp3 deleted file mode 100644 index 56eb28d..0000000 Binary files a/front/assets/piano/ab3.mp3 and /dev/null differ diff --git a/front/assets/piano/ab4.mp3 b/front/assets/piano/ab4.mp3 deleted file mode 100644 index 5094608..0000000 Binary files a/front/assets/piano/ab4.mp3 and /dev/null differ diff --git a/front/assets/piano/ab5.mp3 b/front/assets/piano/ab5.mp3 deleted file mode 100644 index 4b24f22..0000000 Binary files a/front/assets/piano/ab5.mp3 and /dev/null differ diff --git a/front/assets/piano/ab6.mp3 b/front/assets/piano/ab6.mp3 deleted file mode 100644 index 6a37056..0000000 Binary files a/front/assets/piano/ab6.mp3 and /dev/null differ diff --git a/front/assets/piano/ab7.mp3 b/front/assets/piano/ab7.mp3 deleted file mode 100644 index 68cccb0..0000000 Binary files a/front/assets/piano/ab7.mp3 and /dev/null differ diff --git a/front/assets/piano/b0.mp3 b/front/assets/piano/b0.mp3 deleted file mode 100644 index 18ce97f..0000000 Binary files a/front/assets/piano/b0.mp3 and /dev/null differ diff --git a/front/assets/piano/b1.mp3 b/front/assets/piano/b1.mp3 deleted file mode 100644 index 8e1e412..0000000 Binary files a/front/assets/piano/b1.mp3 and /dev/null differ diff --git a/front/assets/piano/b2.mp3 b/front/assets/piano/b2.mp3 deleted file mode 100644 index 68c4287..0000000 Binary files a/front/assets/piano/b2.mp3 and /dev/null differ diff --git a/front/assets/piano/b3.mp3 b/front/assets/piano/b3.mp3 deleted file mode 100644 index 53e5a74..0000000 Binary files a/front/assets/piano/b3.mp3 and /dev/null differ diff --git a/front/assets/piano/b4.mp3 b/front/assets/piano/b4.mp3 deleted file mode 100644 index 07ec753..0000000 Binary files a/front/assets/piano/b4.mp3 and /dev/null differ diff --git a/front/assets/piano/b5.mp3 b/front/assets/piano/b5.mp3 deleted file mode 100644 index 1b1ed5a..0000000 Binary files a/front/assets/piano/b5.mp3 and /dev/null differ diff --git a/front/assets/piano/b6.mp3 b/front/assets/piano/b6.mp3 deleted file mode 100644 index a1dcb4a..0000000 Binary files a/front/assets/piano/b6.mp3 and /dev/null differ diff --git a/front/assets/piano/b7.mp3 b/front/assets/piano/b7.mp3 deleted file mode 100644 index edbc75a..0000000 Binary files a/front/assets/piano/b7.mp3 and /dev/null differ diff --git a/front/assets/piano/bb0.mp3 b/front/assets/piano/bb0.mp3 deleted file mode 100644 index beb17e8..0000000 Binary files a/front/assets/piano/bb0.mp3 and /dev/null differ diff --git a/front/assets/piano/bb1.mp3 b/front/assets/piano/bb1.mp3 deleted file mode 100644 index a96ba58..0000000 Binary files a/front/assets/piano/bb1.mp3 and /dev/null differ diff --git a/front/assets/piano/bb2.mp3 b/front/assets/piano/bb2.mp3 deleted file mode 100644 index 4705e43..0000000 Binary files a/front/assets/piano/bb2.mp3 and /dev/null differ diff --git a/front/assets/piano/bb3.mp3 b/front/assets/piano/bb3.mp3 deleted file mode 100644 index 20181ab..0000000 Binary files a/front/assets/piano/bb3.mp3 and /dev/null differ diff --git a/front/assets/piano/bb4.mp3 b/front/assets/piano/bb4.mp3 deleted file mode 100644 index 390aad9..0000000 Binary files a/front/assets/piano/bb4.mp3 and /dev/null differ diff --git a/front/assets/piano/bb5.mp3 b/front/assets/piano/bb5.mp3 deleted file mode 100644 index 24b5b30..0000000 Binary files a/front/assets/piano/bb5.mp3 and /dev/null differ diff --git a/front/assets/piano/bb6.mp3 b/front/assets/piano/bb6.mp3 deleted file mode 100644 index b5eeee6..0000000 Binary files a/front/assets/piano/bb6.mp3 and /dev/null differ diff --git a/front/assets/piano/bb7.mp3 b/front/assets/piano/bb7.mp3 deleted file mode 100644 index f217e61..0000000 Binary files a/front/assets/piano/bb7.mp3 and /dev/null differ diff --git a/front/assets/piano/c1.mp3 b/front/assets/piano/c1.mp3 deleted file mode 100644 index e053f83..0000000 Binary files a/front/assets/piano/c1.mp3 and /dev/null differ diff --git a/front/assets/piano/c2.mp3 b/front/assets/piano/c2.mp3 deleted file mode 100644 index 5a67b83..0000000 Binary files a/front/assets/piano/c2.mp3 and /dev/null differ diff --git a/front/assets/piano/c3.mp3 b/front/assets/piano/c3.mp3 deleted file mode 100644 index 68a390d..0000000 Binary files a/front/assets/piano/c3.mp3 and /dev/null differ diff --git a/front/assets/piano/c4.mp3 b/front/assets/piano/c4.mp3 deleted file mode 100644 index 9de5479..0000000 Binary files a/front/assets/piano/c4.mp3 and /dev/null differ diff --git a/front/assets/piano/c5.mp3 b/front/assets/piano/c5.mp3 deleted file mode 100644 index 6f37f3a..0000000 Binary files a/front/assets/piano/c5.mp3 and /dev/null differ diff --git a/front/assets/piano/c6.mp3 b/front/assets/piano/c6.mp3 deleted file mode 100644 index ffc5522..0000000 Binary files a/front/assets/piano/c6.mp3 and /dev/null differ diff --git a/front/assets/piano/c7.mp3 b/front/assets/piano/c7.mp3 deleted file mode 100644 index 6b24e1b..0000000 Binary files a/front/assets/piano/c7.mp3 and /dev/null differ diff --git a/front/assets/piano/c8.mp3 b/front/assets/piano/c8.mp3 deleted file mode 100644 index c3e9580..0000000 Binary files a/front/assets/piano/c8.mp3 and /dev/null differ diff --git a/front/assets/piano/d1.mp3 b/front/assets/piano/d1.mp3 deleted file mode 100644 index 590281e..0000000 Binary files a/front/assets/piano/d1.mp3 and /dev/null differ diff --git a/front/assets/piano/d2.mp3 b/front/assets/piano/d2.mp3 deleted file mode 100644 index 7c36581..0000000 Binary files a/front/assets/piano/d2.mp3 and /dev/null differ diff --git a/front/assets/piano/d3.mp3 b/front/assets/piano/d3.mp3 deleted file mode 100644 index b76ea44..0000000 Binary files a/front/assets/piano/d3.mp3 and /dev/null differ diff --git a/front/assets/piano/d4.mp3 b/front/assets/piano/d4.mp3 deleted file mode 100644 index 1528541..0000000 Binary files a/front/assets/piano/d4.mp3 and /dev/null differ diff --git a/front/assets/piano/d5.mp3 b/front/assets/piano/d5.mp3 deleted file mode 100644 index 6378da0..0000000 Binary files a/front/assets/piano/d5.mp3 and /dev/null differ diff --git a/front/assets/piano/d6.mp3 b/front/assets/piano/d6.mp3 deleted file mode 100644 index 7fdbb2e..0000000 Binary files a/front/assets/piano/d6.mp3 and /dev/null differ diff --git a/front/assets/piano/d7.mp3 b/front/assets/piano/d7.mp3 deleted file mode 100644 index ac57399..0000000 Binary files a/front/assets/piano/d7.mp3 and /dev/null differ diff --git a/front/assets/piano/db1.mp3 b/front/assets/piano/db1.mp3 deleted file mode 100644 index 8c620ea..0000000 Binary files a/front/assets/piano/db1.mp3 and /dev/null differ diff --git a/front/assets/piano/db2.mp3 b/front/assets/piano/db2.mp3 deleted file mode 100644 index 2d3c163..0000000 Binary files a/front/assets/piano/db2.mp3 and /dev/null differ diff --git a/front/assets/piano/db3.mp3 b/front/assets/piano/db3.mp3 deleted file mode 100644 index 8270b8e..0000000 Binary files a/front/assets/piano/db3.mp3 and /dev/null differ diff --git a/front/assets/piano/db4.mp3 b/front/assets/piano/db4.mp3 deleted file mode 100644 index d2ec15c..0000000 Binary files a/front/assets/piano/db4.mp3 and /dev/null differ diff --git a/front/assets/piano/db5.mp3 b/front/assets/piano/db5.mp3 deleted file mode 100644 index fe8ad84..0000000 Binary files a/front/assets/piano/db5.mp3 and /dev/null differ diff --git a/front/assets/piano/db6.mp3 b/front/assets/piano/db6.mp3 deleted file mode 100644 index c58b145..0000000 Binary files a/front/assets/piano/db6.mp3 and /dev/null differ diff --git a/front/assets/piano/db7.mp3 b/front/assets/piano/db7.mp3 deleted file mode 100644 index 2dc8fb5..0000000 Binary files a/front/assets/piano/db7.mp3 and /dev/null differ diff --git a/front/assets/piano/e1.mp3 b/front/assets/piano/e1.mp3 deleted file mode 100644 index 5493db7..0000000 Binary files a/front/assets/piano/e1.mp3 and /dev/null differ diff --git a/front/assets/piano/e2.mp3 b/front/assets/piano/e2.mp3 deleted file mode 100644 index b2603c8..0000000 Binary files a/front/assets/piano/e2.mp3 and /dev/null differ diff --git a/front/assets/piano/e3.mp3 b/front/assets/piano/e3.mp3 deleted file mode 100644 index 593ac62..0000000 Binary files a/front/assets/piano/e3.mp3 and /dev/null differ diff --git a/front/assets/piano/e4.mp3 b/front/assets/piano/e4.mp3 deleted file mode 100644 index 1ad3cba..0000000 Binary files a/front/assets/piano/e4.mp3 and /dev/null differ diff --git a/front/assets/piano/e5.mp3 b/front/assets/piano/e5.mp3 deleted file mode 100644 index 6e6372b..0000000 Binary files a/front/assets/piano/e5.mp3 and /dev/null differ diff --git a/front/assets/piano/e6.mp3 b/front/assets/piano/e6.mp3 deleted file mode 100644 index d7134e1..0000000 Binary files a/front/assets/piano/e6.mp3 and /dev/null differ diff --git a/front/assets/piano/e7.mp3 b/front/assets/piano/e7.mp3 deleted file mode 100644 index d5fbeb9..0000000 Binary files a/front/assets/piano/e7.mp3 and /dev/null differ diff --git a/front/assets/piano/eb1.mp3 b/front/assets/piano/eb1.mp3 deleted file mode 100644 index 0516dd0..0000000 Binary files a/front/assets/piano/eb1.mp3 and /dev/null differ diff --git a/front/assets/piano/eb2.mp3 b/front/assets/piano/eb2.mp3 deleted file mode 100644 index d4cef5a..0000000 Binary files a/front/assets/piano/eb2.mp3 and /dev/null differ diff --git a/front/assets/piano/eb3.mp3 b/front/assets/piano/eb3.mp3 deleted file mode 100644 index 3bae63f..0000000 Binary files a/front/assets/piano/eb3.mp3 and /dev/null differ diff --git a/front/assets/piano/eb4.mp3 b/front/assets/piano/eb4.mp3 deleted file mode 100644 index 3a2dfe3..0000000 Binary files a/front/assets/piano/eb4.mp3 and /dev/null differ diff --git a/front/assets/piano/eb5.mp3 b/front/assets/piano/eb5.mp3 deleted file mode 100644 index b25bc2f..0000000 Binary files a/front/assets/piano/eb5.mp3 and /dev/null differ diff --git a/front/assets/piano/eb6.mp3 b/front/assets/piano/eb6.mp3 deleted file mode 100644 index 5626c0a..0000000 Binary files a/front/assets/piano/eb6.mp3 and /dev/null differ diff --git a/front/assets/piano/eb7.mp3 b/front/assets/piano/eb7.mp3 deleted file mode 100644 index de86659..0000000 Binary files a/front/assets/piano/eb7.mp3 and /dev/null differ diff --git a/front/assets/piano/f1.mp3 b/front/assets/piano/f1.mp3 deleted file mode 100644 index ca852f8..0000000 Binary files a/front/assets/piano/f1.mp3 and /dev/null differ diff --git a/front/assets/piano/f2.mp3 b/front/assets/piano/f2.mp3 deleted file mode 100644 index 439a600..0000000 Binary files a/front/assets/piano/f2.mp3 and /dev/null differ diff --git a/front/assets/piano/f3.mp3 b/front/assets/piano/f3.mp3 deleted file mode 100644 index e96cb25..0000000 Binary files a/front/assets/piano/f3.mp3 and /dev/null differ diff --git a/front/assets/piano/f4.mp3 b/front/assets/piano/f4.mp3 deleted file mode 100644 index 2e56f1c..0000000 Binary files a/front/assets/piano/f4.mp3 and /dev/null differ diff --git a/front/assets/piano/f5.mp3 b/front/assets/piano/f5.mp3 deleted file mode 100644 index 8888a19..0000000 Binary files a/front/assets/piano/f5.mp3 and /dev/null differ diff --git a/front/assets/piano/f6.mp3 b/front/assets/piano/f6.mp3 deleted file mode 100644 index 57cac7b..0000000 Binary files a/front/assets/piano/f6.mp3 and /dev/null differ diff --git a/front/assets/piano/f7.mp3 b/front/assets/piano/f7.mp3 deleted file mode 100644 index 976012b..0000000 Binary files a/front/assets/piano/f7.mp3 and /dev/null differ diff --git a/front/assets/piano/g1.mp3 b/front/assets/piano/g1.mp3 deleted file mode 100644 index 07b425d..0000000 Binary files a/front/assets/piano/g1.mp3 and /dev/null differ diff --git a/front/assets/piano/g2.mp3 b/front/assets/piano/g2.mp3 deleted file mode 100644 index e633621..0000000 Binary files a/front/assets/piano/g2.mp3 and /dev/null differ diff --git a/front/assets/piano/g3.mp3 b/front/assets/piano/g3.mp3 deleted file mode 100644 index 4f6a675..0000000 Binary files a/front/assets/piano/g3.mp3 and /dev/null differ diff --git a/front/assets/piano/g4.mp3 b/front/assets/piano/g4.mp3 deleted file mode 100644 index f0aadf7..0000000 Binary files a/front/assets/piano/g4.mp3 and /dev/null differ diff --git a/front/assets/piano/g5.mp3 b/front/assets/piano/g5.mp3 deleted file mode 100644 index 25ad6b1..0000000 Binary files a/front/assets/piano/g5.mp3 and /dev/null differ diff --git a/front/assets/piano/g6.mp3 b/front/assets/piano/g6.mp3 deleted file mode 100644 index 91f7e49..0000000 Binary files a/front/assets/piano/g6.mp3 and /dev/null differ diff --git a/front/assets/piano/g7.mp3 b/front/assets/piano/g7.mp3 deleted file mode 100644 index 67a2287..0000000 Binary files a/front/assets/piano/g7.mp3 and /dev/null differ diff --git a/front/assets/piano/gb1.mp3 b/front/assets/piano/gb1.mp3 deleted file mode 100644 index fff4fb5..0000000 Binary files a/front/assets/piano/gb1.mp3 and /dev/null differ diff --git a/front/assets/piano/gb2.mp3 b/front/assets/piano/gb2.mp3 deleted file mode 100644 index 4ae7cd4..0000000 Binary files a/front/assets/piano/gb2.mp3 and /dev/null differ diff --git a/front/assets/piano/gb3.mp3 b/front/assets/piano/gb3.mp3 deleted file mode 100644 index 5c10e00..0000000 Binary files a/front/assets/piano/gb3.mp3 and /dev/null differ diff --git a/front/assets/piano/gb4.mp3 b/front/assets/piano/gb4.mp3 deleted file mode 100644 index bd057b1..0000000 Binary files a/front/assets/piano/gb4.mp3 and /dev/null differ diff --git a/front/assets/piano/gb5.mp3 b/front/assets/piano/gb5.mp3 deleted file mode 100644 index 97edeb9..0000000 Binary files a/front/assets/piano/gb5.mp3 and /dev/null differ diff --git a/front/assets/piano/gb6.mp3 b/front/assets/piano/gb6.mp3 deleted file mode 100644 index 44afd1e..0000000 Binary files a/front/assets/piano/gb6.mp3 and /dev/null differ diff --git a/front/assets/piano/gb7.mp3 b/front/assets/piano/gb7.mp3 deleted file mode 100644 index 9d5f354..0000000 Binary files a/front/assets/piano/gb7.mp3 and /dev/null differ diff --git a/front/components/CompetenciesTable.tsx b/front/components/CompetenciesTable.tsx index d0874da..e51f478 100644 --- a/front/components/CompetenciesTable.tsx +++ b/front/components/CompetenciesTable.tsx @@ -15,7 +15,7 @@ type CompetenciesTableProps = { const CompetenciesTable = (props: CompetenciesTableProps) => { const navigation = useNavigation(); return ( - navigation.navigate('User', {})} shadow={3}> + navigation.navigate('User')} shadow={3}> {Object.keys(props).map((competencyName, i) => ( diff --git a/front/components/Play/PartitionMagic.tsx b/front/components/Play/PartitionMagic.tsx index cef984c..a3d95e6 100644 --- a/front/components/Play/PartitionMagic.tsx +++ b/front/components/Play/PartitionMagic.tsx @@ -1,21 +1,23 @@ import * as React from 'react'; -import { View } from 'react-native'; +import { Platform, View } from 'react-native'; import API from '../../API'; import { useQuery } from '../../Queries'; import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated'; import { CursorInfoItem } from '../../models/SongCursorInfos'; -import { PianoNotes } from '../../state/SoundPlayerSlice'; import { Audio } from 'expo-av'; import { SvgContainer } from './SvgContainer'; import LoadingComponent from '../Loading'; +import { SplendidGrandPiano } from 'smplr'; -// note we are also using timestamp in a context export type ParitionMagicProps = { timestamp: number; songID: number; + shouldPlay: boolean; onEndReached: () => void; - onError?: (err: string) => void; - onReady?: () => void; + onError: (err: string) => void; + onReady: () => void; + onPlay: () => void; + onPause: () => void; }; const getSVGURL = (songID: number) => { @@ -39,19 +41,26 @@ const getCursorToPlay = ( } }; +const transitionDuration = 50; + const PartitionMagic = ({ timestamp, songID, + shouldPlay, onEndReached, onError, onReady, + onPlay, + onPause, }: ParitionMagicProps) => { const { data, isLoading, isError } = useQuery(API.getSongCursorInfos(songID)); const currentCurIdx = React.useRef(-1); const [endPartitionReached, setEndPartitionReached] = React.useState(false); const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false); const partitionOffset = useSharedValue(0); - const pianoSounds = React.useRef | null>(null); + const melodySound = React.useRef(null); + const piano = React.useRef(null); + const [isPianoLoaded, setIsPianoLoaded] = React.useState(false); const cursorPaddingVertical = 10; const cursorPaddingHorizontal = 3; @@ -70,22 +79,36 @@ const PartitionMagic = ({ } React.useEffect(() => { - if (!pianoSounds.current) { - Promise.all( - Object.entries(PianoNotes).map(([midiNumber, noteResource]) => - Audio.Sound.createAsync(noteResource, { - volume: 1, - progressUpdateIntervalMillis: 100, - }).then((sound) => [midiNumber, sound.sound] as const) - ) - ).then((res) => { - pianoSounds.current = res.reduce( - (prev, curr) => ({ ...prev, [curr[0]]: curr[1] }), - {} - ); - console.log('sound loaded'); + if (Platform.OS === 'web' && !piano.current) { + const audio = new AudioContext(); + piano.current = new SplendidGrandPiano(audio); + piano.current.load.then(() => { + setIsPianoLoaded(true); + }); + } else if (!melodySound.current) { + Audio.Sound.createAsync( + { + uri: API.getPartitionMelodyUrl(songID), + }, + { + progressUpdateIntervalMillis: 200, + } + ).then((track) => { + melodySound.current = track.sound; }); } + return () => { + if (melodySound.current) { + melodySound.current.pauseAsync(); + melodySound.current.unloadAsync(); + melodySound.current = null; + } + if (piano.current) { + piano.current.stop(); + piano.current.context.close(); + piano.current = null; + } + }; }, []); const partitionDims = React.useMemo<[number, number]>(() => { return [data?.pageWidth ?? 0, data?.pageHeight ?? 1]; @@ -99,47 +122,88 @@ const PartitionMagic = ({ }, [onError, isError]); React.useEffect(() => { - if (onReady && isPartitionSvgLoaded && !isLoading) { + if (isPartitionSvgLoaded && !isLoading && (melodySound.current?._loaded || isPianoLoaded)) { onReady(); } - }, [onReady, isPartitionSvgLoaded, isLoading]); + }, [isPartitionSvgLoaded, isLoading, melodySound.current?._loaded, isPianoLoaded]); + + React.useEffect(() => { + if (Platform.OS === 'web') { + if (!piano.current || !isPianoLoaded) { + return; + } + shouldPlay ? onPlay() : onPause(); + return; + } + if (!melodySound.current || !melodySound.current._loaded) { + return; + } + if (shouldPlay) { + melodySound.current.playAsync().then(onPlay).catch(console.error); + } else { + melodySound.current.pauseAsync().then(onPause).catch(console.error); + } + }, [shouldPlay]); React.useEffect(() => { if (endPartitionReached) { + // if the audio is unsync + melodySound.current?.pauseAsync(); onEndReached(); } }, [endPartitionReached]); - const transitionDuration = 200; + React.useEffect(() => { + if (!melodySound.current || !melodySound.current._loaded) return; + if (!data || data?.cursors.length === 0) return; - getCursorToPlay( - data?.cursors ?? [], - currentCurIdx.current, - timestamp - transitionDuration, - (cursor, idx) => { - currentCurIdx.current = idx; - if (pianoSounds.current) { - cursor.notes.forEach(({ note, duration }) => { - try { - const sound = pianoSounds.current![note]!; - sound.playAsync().catch(console.error); - setTimeout(() => { - sound.stopAsync(); - }, duration - 10); - } catch (e) { - console.log(e); - } - }); - } - partitionOffset.value = withTiming( - -(cursor.x - data!.cursors[0]!.x) / partitionDims[0], - { - duration: transitionDuration, - easing: Easing.inOut(Easing.ease), + melodySound.current.setOnPlaybackStatusUpdate((status) => { + //@ts-expect-error positionMillis is not in the type + const timestamp = status?.positionMillis ?? 0; + getCursorToPlay( + data!.cursors, + currentCurIdx.current, + timestamp + transitionDuration, + (cursor, idx) => { + currentCurIdx.current = idx; + partitionOffset.value = withTiming( + -(cursor.x - data!.cursors[0]!.x) / partitionDims[0], + { + duration: transitionDuration, + easing: Easing.inOut(Easing.ease), + } + ); } ); - } - ); + }); + }, [data?.cursors, melodySound.current?._loaded]); + + React.useEffect(() => { + if (!shouldPlay) return; + if (!piano.current || !isPianoLoaded) return; + if (!data || data?.cursors.length === 0) return; + getCursorToPlay( + data!.cursors, + currentCurIdx.current, + timestamp + transitionDuration, + (cursor, idx) => { + currentCurIdx.current = idx; + partitionOffset.value = withTiming( + -(cursor.x - data!.cursors[0]!.x) / partitionDims[0], + { + duration: transitionDuration, + easing: Easing.inOut(Easing.ease), + } + ); + cursor.notes.forEach((note) => { + piano.current?.start({ + note: note.note, + duration: note.duration, + }); + }); + } + ); + }, [timestamp, data?.cursors, isPianoLoaded]); return ( {}, + onReady: () => {}, + onPlay: () => {}, + onPause: () => {}, +}; + export default PartitionMagic; diff --git a/front/components/Play/PlayScore.tsx b/front/components/Play/PlayScore.tsx new file mode 100644 index 0000000..a911e21 --- /dev/null +++ b/front/components/Play/PlayScore.tsx @@ -0,0 +1,105 @@ +import React, { useEffect } from 'react'; +import { View } from 'react-native'; +import { Text, useTheme } from 'native-base'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSequence, + withTiming, + withDelay, + Easing, +} from 'react-native-reanimated'; +import { ColorSchemeType } from 'native-base/lib/typescript/components/types'; + +export type ScoreMessage = { + content: string; + color?: ColorSchemeType; + id: number; +}; + +type PlayScoreProps = { + score: number; + streak: number; + message?: ScoreMessage; +}; + +export const PlayScore = ({ score, streak, message }: PlayScoreProps) => { + const scoreMessageScale = useSharedValue(0); + // this style should bounce in on enter and fade away + const scoreMsgStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: scoreMessageScale.value }], + }; + }); + const { colors } = useTheme(); + const textColor = colors.text; + + useEffect(() => { + if (message) { + scoreMessageScale.value = withSequence( + withTiming(1, { + duration: 400, + easing: Easing.elastic(3), + }), + withDelay( + 700, + withTiming(0, { + duration: 300, + easing: Easing.out(Easing.cubic), + }) + ) + ); + } + }, [message]); + + return ( + + + + {score} + + + {message && ( + + + + {message.content} + + {streak > 0 && ( + + {`x${streak}`} + + )} + + + )} + + ); +}; diff --git a/front/components/Play/PlayViewControlBar.tsx b/front/components/Play/PlayViewControlBar.tsx index 0d34493..1d1ff75 100644 --- a/front/components/Play/PlayViewControlBar.tsx +++ b/front/components/Play/PlayViewControlBar.tsx @@ -1,7 +1,6 @@ import { View } from 'react-native'; import * as React from 'react'; -import { Row, Image, Text, Icon, useBreakpointValue } from 'native-base'; -import IconButton from '../IconButton'; +import { Row, Image, Text, useBreakpointValue, IconButton } from 'native-base'; import { Ionicons } from '@expo/vector-icons'; import { MetronomeControls } from '../Metronome'; import StarProgress from '../StarProgress'; @@ -112,30 +111,22 @@ const PlayViewControlBar = ({ size="sm" variant="solid" disabled={disabled} - icon={ - - } - onPress={() => { - if (paused) { - onResume(); - } else { - onPause(); - } + _icon={{ + as: Ionicons, + color: colors.coolGray[900], + name: paused ? 'play' : 'pause', }} + onPress={paused ? onResume : onPause} /> } - onPress={() => { - onEnd(); + _icon={{ + as: Ionicons, + name: 'stop', }} + onPress={onEnd} /> {time < 0 diff --git a/front/components/ProgressBar.tsx b/front/components/ProgressBar.tsx index 325567c..ca2bfc6 100644 --- a/front/components/ProgressBar.tsx +++ b/front/components/ProgressBar.tsx @@ -14,7 +14,7 @@ const ProgressBar = ({ xp }: { xp: number }) => { const nav = useNavigation(); return ( - nav.navigate('User', {})}> + nav.navigate('User')}> diff --git a/front/components/ScoreModal.tsx b/front/components/ScoreModal.tsx index 58a39e5..fc15e0a 100644 --- a/front/components/ScoreModal.tsx +++ b/front/components/ScoreModal.tsx @@ -3,7 +3,6 @@ import ButtonBase from './UI/ButtonBase'; import { Translate, TranslationKey, translate } from '../i18n/i18n'; import { Play, Star1 } from 'iconsax-react-native'; import { useNavigation } from '../Navigation'; -import { StackActions } from '@react-navigation/native'; type ScoreModalProps = { songId: number; @@ -22,21 +21,6 @@ type ScoreModalProps = { }; const ScoreModal = (props: ScoreModalProps) => { - // const props = { - // songId: 1, - // overallScore: 74, - // precision: 0, - // score: { - // missed: 9, - // good: 1, - // great: 2, - // perfect: 4, - // wrong: 0, - // max_score: 100, - // current_streak: 1, - // max_streak: 11, - // } as const - // } as const; //TODO DELETE ME const navigation = useNavigation(); const theme = useTheme(); const score = (props.overallScore * 100) / props.score.max_score; @@ -96,20 +80,18 @@ const ScoreModal = (props: ScoreModalProps) => { icon={Play} type="outlined" title={translate('playAgain')} - onPress={() => - navigation.dispatch(StackActions.replace('Play', { songId: props.songId })) - } + onPress={() => navigation.replace('Play', { songId: props.songId })} /> + onPress={() => { navigation.canGoBack() ? navigation.goBack() - : navigation.navigate('Home', {}) - } + : navigation.replace('Tabs', { screen: 'Home' }); + }} /> diff --git a/front/components/UI/ButtonBase.tsx b/front/components/UI/ButtonBase.tsx index 833835c..b1bd0a9 100644 --- a/front/components/UI/ButtonBase.tsx +++ b/front/components/UI/ButtonBase.tsx @@ -11,6 +11,7 @@ interface ButtonProps { title?: string; style?: StyleProp; onPress?: () => void | Promise; + onLongPress?: () => void | Promise; isDisabled?: boolean; icon?: Icon; iconVariant?: 'Bold' | 'Outline'; @@ -22,6 +23,7 @@ const ButtonBase: React.FC = ({ title, style, onPress, + onLongPress, isDisabled, icon, iconImage, @@ -123,6 +125,18 @@ const ButtonBase: React.FC = ({ } } }} + onLongPress={async () => { + if (onLongPress && !isDisabled) { + setLoading(true); + try { + await onLongPress(); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } + }} isDisabled={isDisabled} isOutlined={type === 'outlined'} > diff --git a/front/components/UI/IconButton.tsx b/front/components/UI/IconButton.tsx index a2c9d02..a012ac8 100644 --- a/front/components/UI/IconButton.tsx +++ b/front/components/UI/IconButton.tsx @@ -30,7 +30,7 @@ type IconButtonProps = { /** * Callback function triggered when the button is pressed. */ - onPress?: () => void | Promise; + onPress?: (state: boolean) => void | Promise; /** * Size of the icon. @@ -183,7 +183,7 @@ const IconButton: React.FC = ({ const toggleState = async () => { // Execute onPress if provided. if (onPress) { - await onPress(); + await onPress(!isActiveState); } // Toggle isActiveState. diff --git a/front/components/UI/InteractiveBase.tsx b/front/components/UI/InteractiveBase.tsx index 4bf7288..16d1916 100644 --- a/front/components/UI/InteractiveBase.tsx +++ b/front/components/UI/InteractiveBase.tsx @@ -5,6 +5,7 @@ import { Animated, StyleProp, ViewStyle } from 'react-native'; interface InteractiveBaseProps { children?: React.ReactNode; onPress?: () => Promise; + onLongPress?: () => Promise; isDisabled?: boolean; isOutlined?: boolean; focusable?: boolean; @@ -44,6 +45,7 @@ interface InteractiveBaseProps { const InteractiveBase: React.FC = ({ children, onPress, + onLongPress, style, styleAnimate, isDisabled = false, @@ -183,10 +185,6 @@ const InteractiveBase: React.FC = ({ useNativeDriver: false, }), ]).start(); - - if (onPress && !isDisabled) { - onPress(); - } }; // Mouse Leave const handleMouseLeave = () => { @@ -248,6 +246,8 @@ const InteractiveBase: React.FC = ({ onPressIn={handlePressIn} onPressOut={handlePressOut} onHoverOut={handleMouseLeave} + onPress={onPress} + onLongPress={onLongPress} > {children} diff --git a/front/components/UI/MusicItem.tsx b/front/components/UI/MusicItem.tsx index 804f50a..8d12953 100644 --- a/front/components/UI/MusicItem.tsx +++ b/front/components/UI/MusicItem.tsx @@ -33,7 +33,7 @@ export interface MusicItemType { style?: ViewStyle | ViewStyle[]; /** Callback function triggered when the like button is pressed. */ - onLike: () => void; + onLike: (state: boolean) => void; /** Callback function triggered when the song is played. */ onPlay: () => void; diff --git a/front/components/UI/MusicList.tsx b/front/components/UI/MusicList.tsx index 4272ccd..c3a2bd0 100644 --- a/front/components/UI/MusicList.tsx +++ b/front/components/UI/MusicList.tsx @@ -194,7 +194,6 @@ function MusicListComponent({ // FlatList: Renders list efficiently, only rendering visible items. return ( { - const userQuery = useQuery(API.getUserInfo); - const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); - - if (!userQuery.data || userQuery.isLoading) { - return ; - } - const colorScheme = useColorScheme(); - const [logo] = useAssets( - colorScheme == 'light' - ? require('../../assets/icon_light.png') - : require('../../assets/icon_dark.png') - ); - - return ( - - {screenSize === 'small' ? ( - - {children} - - ) : ( - - {children} - - )} - {colorScheme === 'dark' && ( - - )} - - ); -}; - -export default ScaffoldCC; diff --git a/front/components/UI/ScaffoldDesktopCC.tsx b/front/components/UI/ScaffoldDesktopCC.tsx index 0ec7dbb..fae492c 100644 --- a/front/components/UI/ScaffoldDesktopCC.tsx +++ b/front/components/UI/ScaffoldDesktopCC.tsx @@ -1,31 +1,19 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { View, Image, Pressable } from 'react-native'; +import { View, Image, Pressable, useColorScheme, ViewStyle } from 'react-native'; import { Divider, Text, ScrollView, Row, useMediaQuery, useTheme } from 'native-base'; +import { useAssets } from 'expo-asset'; import { useQuery } from '../../Queries'; import API from '../../API'; import ButtonBase from './ButtonBase'; import { Icon } from 'iconsax-react-native'; import { LoadingView } from '../Loading'; -import { Translate, TranslationKey, translate } from '../../i18n/i18n'; +import { Translate, translate } from '../../i18n/i18n'; import { useNavigation } from '../../Navigation'; import Spacer from './Spacer'; -import User from '../../models/User'; import LogoutButtonCC from './LogoutButtonCC'; -import GlassmorphismCC from './Glassmorphism'; - -type ScaffoldDesktopCCProps = { - widthPadding: boolean; - children?: React.ReactNode; - user: User; - logo: string; - routeName: string; - menu: readonly { - type: 'main' | 'sub'; - title: TranslationKey; - icon: Icon; - link: string; - }[]; -}; +import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; +import { ReactElement } from 'react'; +import { NavigationState } from '@react-navigation/native'; // TODO a tester avec un historique de plus de 3 musics différente mdr !! const SongHistory = (props: { quantity: number }) => { @@ -81,18 +69,72 @@ const SongHistory = (props: { quantity: number }) => { ); }; -const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => { - const navigation = useNavigation(); - const userQuery = useQuery(API.getUserInfo); +const NavigationButton = ({ + isSmallScreen, + label, + icon, + isFocused, + navigation, + route, +}: { + isSmallScreen: boolean; + label: string; + icon?: Icon; + isFocused: boolean; + navigation: BottomTabBarProps['navigation']; + route: NavigationState['routes'][0]; +}) => { + return ( + + { + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }); + + if (!isFocused && !event.defaultPrevented) { + navigation.navigate(route.name, route.params); + } + }} + onLongPress={() => { + navigation.emit({ + type: 'tabLongPress', + target: route.key, + }); + }} + /> + + + ); +}; + +const ScaffoldDesktopCC = ({ + state, + descriptors, + navigation, + children, + style, +}: Omit & { children: ReactElement; style?: ViewStyle }) => { + const user = useQuery(API.getUserInfo); const [isSmallScreen] = useMediaQuery({ maxWidth: 1100 }); const { colors } = useTheme(); - - if (!userQuery.data || userQuery.isLoading) { - return ; - } + const colorScheme = useColorScheme(); + const [logo] = useAssets( + colorScheme == 'light' + ? require('../../assets/icon_light.png') + : require('../../assets/icon_dark.png') + ); return ( - + { style={{ justifyContent: isSmallScreen ? 'center' : 'flex-start' }} > { - {props.menu.map( - (value) => - value.type === 'main' && ( - - - navigation.navigate(value.link as never) - } - /> - - - ) - )} + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]!; + + if ((options as any).subMenu) return null; + return ( + + ); + })} {!isSmallScreen && ( @@ -182,60 +207,45 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => { - {props.menu.map( - (value) => - value.type === 'sub' && ( - navigation.navigate(value.link as never)} - /> - ) - )} + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]!; + + if (!(options as any).subMenu) return null; + return ( + + ); + })} - - - {props.children} - - + ); diff --git a/front/components/UI/ScaffoldMobileCC.tsx b/front/components/UI/ScaffoldMobileCC.tsx index c4c1070..dd346f3 100644 --- a/front/components/UI/ScaffoldMobileCC.tsx +++ b/front/components/UI/ScaffoldMobileCC.tsx @@ -1,79 +1,71 @@ -/* eslint-disable no-mixed-spaces-and-tabs */ import { View } from 'react-native'; import { Flex, useMediaQuery, useTheme } from 'native-base'; import ButtonBase from './ButtonBase'; import { Icon } from 'iconsax-react-native'; -import { useNavigation } from '../../Navigation'; -import User from '../../models/User'; import { translate } from '../../i18n/i18n'; +import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; +import { ComponentProps } from 'react'; -type ScaffoldMobileCCProps = { - children?: React.ReactNode; - user: User; - logo: string; - routeName: string; - widthPadding: boolean; - enableScroll: boolean; - menu: readonly { - type: 'main' | 'sub'; - title: - | 'menuDiscovery' - | 'menuProfile' - | 'menuMusic' - | 'menuSearch' - | 'menuLeaderBoard' - | 'menuSettings'; - icon: Icon; - link: string; - }[]; -}; - -const ScaffoldMobileCC = (props: ScaffoldMobileCCProps) => { - const navigation = useNavigation(); +const ScaffoldMobileCC = ({ state, descriptors, navigation }: BottomTabBarProps) => { const [isSmallScreen] = useMediaQuery({ maxWidth: 400 }); const { colors } = useTheme(); return ( - - + - {props.children} - - - - {props.menu.map((value) => ( + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]!; + const label = options.title !== undefined ? options.title : route.name; + const icon = options.tabBarIcon as Icon; + const isFocused = state.index === index; + + return ( navigation.navigate(value.link as never)} + isDisabled={isFocused} + iconVariant={isFocused ? 'Bold' : 'Outline'} + onPress={() => { + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }); + + if (!isFocused && !event.defaultPrevented) { + navigation.navigate(route.name, route.params); + } + }} + onLongPress={() => { + navigation.emit({ + type: 'tabLongPress', + target: route.key, + }); + }} /> - ))} - - + ); + })} + ); }; -export default ScaffoldMobileCC; +// This is needed to bypass a bug in react-navigation that calls custom tabBars weirdly +const Wrapper = (props: ComponentProps) => { + return ; +}; + +export default Wrapper; diff --git a/front/i18n/Translations.ts b/front/i18n/Translations.ts index e208b45..6c65c82 100644 --- a/front/i18n/Translations.ts +++ b/front/i18n/Translations.ts @@ -646,7 +646,7 @@ export const fr: typeof en = { updateProfile: 'Changer le Profile', accountCreatedOn: 'Compte créé le', downloadAPKInstructions: - "Descargue 'android-build.apk' en la sección 'Assets' de la última versión.", + "Télécharger 'android-build.apk' dans la section 'Assets' de la dernière release", }; export const sp: typeof en = { @@ -979,5 +979,5 @@ export const sp: typeof en = { updateProfile: 'Cambiar el perfil', accountCreatedOn: 'Cuenta creada el', downloadAPKInstructions: - "Télécharger 'android-build.apk' dans la section 'Assets' de la dernière release", + "Descargue 'android-build.apk' en la sección 'Assets' de la última versión", }; diff --git a/front/package.json b/front/package.json index ab42657..83ef69b 100644 --- a/front/package.json +++ b/front/package.json @@ -21,6 +21,7 @@ "@expo/webpack-config": "^19.0.0", "@motiz88/react-native-midi": "^0.0.6", "@react-native-async-storage/async-storage": "1.18.2", + "@react-navigation/bottom-tabs": "^6.5.11", "@react-navigation/native": "^6.1.8", "@react-navigation/native-stack": "^6.9.14", "@reduxjs/toolkit": "^1.9.6", @@ -59,6 +60,7 @@ "react-redux": "^8.1.2", "react-use-precision-timer": "^3.3.1", "redux-persist": "^6.0.0", + "smplr": "^0.12.1", "type-fest": "^4.3.2", "url": "^0.11.3", "yup": "^1.3.1" diff --git a/front/state/SoundPlayerSlice.ts b/front/state/SoundPlayerSlice.ts deleted file mode 100644 index 97b63f1..0000000 --- a/front/state/SoundPlayerSlice.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Audio } from 'expo-av'; - -type MidiNumber = number; - -// Source: https://computermusicresource.com/midikeys.html -// Deserve an extra credit for doing this by hand -// The value is the value returned by `required`, needed by Expo to load/play the sound -export const PianoNotes = { - 33: require('../assets/piano/a0.mp3'), - 45: require('../assets/piano/a1.mp3'), - 57: require('../assets/piano/a2.mp3'), - 69: require('../assets/piano/a3.mp3'), - 81: require('../assets/piano/a4.mp3'), - 93: require('../assets/piano/a5.mp3'), - 105: require('../assets/piano/a6.mp3'), - 117: require('../assets/piano/a7.mp3'), - 44: require('../assets/piano/ab1.mp3'), - 56: require('../assets/piano/ab2.mp3'), - 68: require('../assets/piano/ab3.mp3'), - 80: require('../assets/piano/ab4.mp3'), - 92: require('../assets/piano/ab5.mp3'), - 104: require('../assets/piano/ab6.mp3'), - 116: require('../assets/piano/ab7.mp3'), - 35: require('../assets/piano/b0.mp3'), - 47: require('../assets/piano/b1.mp3'), - 59: require('../assets/piano/b2.mp3'), - 71: require('../assets/piano/b3.mp3'), - 83: require('../assets/piano/b4.mp3'), - 95: require('../assets/piano/b5.mp3'), - 107: require('../assets/piano/b6.mp3'), - 119: require('../assets/piano/b7.mp3'), - 34: require('../assets/piano/bb0.mp3'), - 46: require('../assets/piano/bb1.mp3'), - 58: require('../assets/piano/bb2.mp3'), - 70: require('../assets/piano/bb3.mp3'), - 82: require('../assets/piano/bb4.mp3'), - 94: require('../assets/piano/bb5.mp3'), - 106: require('../assets/piano/bb6.mp3'), - 118: require('../assets/piano/bb7.mp3'), - 36: require('../assets/piano/c1.mp3'), - 48: require('../assets/piano/c2.mp3'), - 60: require('../assets/piano/c3.mp3'), - 72: require('../assets/piano/c4.mp3'), - 84: require('../assets/piano/c5.mp3'), - 96: require('../assets/piano/c6.mp3'), - 108: require('../assets/piano/c7.mp3'), - 120: require('../assets/piano/c8.mp3'), - 38: require('../assets/piano/d1.mp3'), - 50: require('../assets/piano/d2.mp3'), - 62: require('../assets/piano/d3.mp3'), - 74: require('../assets/piano/d4.mp3'), - 86: require('../assets/piano/d5.mp3'), - 98: require('../assets/piano/d6.mp3'), - 110: require('../assets/piano/d7.mp3'), - 37: require('../assets/piano/db1.mp3'), - 49: require('../assets/piano/db2.mp3'), - 61: require('../assets/piano/db3.mp3'), - 73: require('../assets/piano/db4.mp3'), - 85: require('../assets/piano/db5.mp3'), - 97: require('../assets/piano/db6.mp3'), - 109: require('../assets/piano/db7.mp3'), - 40: require('../assets/piano/e1.mp3'), - 52: require('../assets/piano/e2.mp3'), - 64: require('../assets/piano/e3.mp3'), - 76: require('../assets/piano/e4.mp3'), - 88: require('../assets/piano/e5.mp3'), - 100: require('../assets/piano/e6.mp3'), - 112: require('../assets/piano/e7.mp3'), - 39: require('../assets/piano/eb1.mp3'), - 51: require('../assets/piano/eb2.mp3'), - 63: require('../assets/piano/eb3.mp3'), - 75: require('../assets/piano/eb4.mp3'), - 87: require('../assets/piano/eb5.mp3'), - 99: require('../assets/piano/eb6.mp3'), - 111: require('../assets/piano/eb7.mp3'), - 41: require('../assets/piano/f1.mp3'), - 53: require('../assets/piano/f2.mp3'), - 65: require('../assets/piano/f3.mp3'), - 77: require('../assets/piano/f4.mp3'), - 89: require('../assets/piano/f5.mp3'), - 101: require('../assets/piano/f6.mp3'), - 113: require('../assets/piano/f7.mp3'), - 43: require('../assets/piano/g1.mp3'), - 55: require('../assets/piano/g2.mp3'), - 67: require('../assets/piano/g3.mp3'), - 79: require('../assets/piano/g4.mp3'), - 91: require('../assets/piano/g5.mp3'), - 103: require('../assets/piano/g6.mp3'), - 115: require('../assets/piano/g7.mp3'), - 42: require('../assets/piano/gb1.mp3'), - 54: require('../assets/piano/gb2.mp3'), - 66: require('../assets/piano/gb3.mp3'), - 78: require('../assets/piano/gb4.mp3'), - 90: require('../assets/piano/gb5.mp3'), - 102: require('../assets/piano/gb6.mp3'), - 114: require('../assets/piano/gb7.mp3'), -} as const; - -export type Sounds = Record; - -export const soundPlayerSlice = createSlice({ - name: 'soundPlayer', - initialState: { - sounds: undefined as Sounds | undefined, - }, - reducers: { - setSounds: (state, action: PayloadAction) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-expect-error - state.sounds = action.payload; - }, - unsetSounds: (state) => { - Object.entries(state.sounds ?? {}).map((sound) => sound[1].unloadAsync()); - state.sounds = undefined; - }, - }, -}); -export const { setSounds, unsetSounds } = soundPlayerSlice.actions; -export default soundPlayerSlice.reducer; diff --git a/front/state/Store.ts b/front/state/Store.ts index 853871b..f39440e 100644 --- a/front/state/Store.ts +++ b/front/state/Store.ts @@ -1,6 +1,5 @@ import userReducer from '../state/UserSlice'; import settingsReducer from './SettingsSlice'; -import SoundPlayerSliceReducer from './SoundPlayerSlice'; import { StateFromReducersMapObject, configureStore } from '@reduxjs/toolkit'; import languageReducer from './LanguageSlice'; import { @@ -30,7 +29,6 @@ const reducers = { user: userReducer, language: languageReducer, settings: settingsReducer, - soundPlayer: SoundPlayerSliceReducer, }; type State = StateFromReducersMapObject; diff --git a/front/utils/navigator.tsx b/front/utils/navigator.tsx new file mode 100644 index 0000000..4632dfc --- /dev/null +++ b/front/utils/navigator.tsx @@ -0,0 +1,126 @@ +import { + createNavigatorFactory, + type DefaultNavigatorOptions, + type ParamListBase, + type TabActionHelpers, + type TabNavigationState, + TabRouter, + type TabRouterOptions, + useNavigationBuilder, +} from '@react-navigation/native'; +import * as React from 'react'; + +import type { BottomTabNavigationConfig } from '@react-navigation/bottom-tabs/src/types'; +import type { + BottomTabHeaderProps, + BottomTabNavigationEventMap, + BottomTabNavigationOptions, + BottomTabNavigationProp, +} from '@react-navigation/bottom-tabs'; +import { BottomTabView } from '@react-navigation/bottom-tabs'; + +import { Screen, Header, getHeaderTitle, SafeAreaProviderCompat } from '@react-navigation/elements'; + +import ScaffoldMobileCC from '../components/UI/ScaffoldMobileCC'; +import { useBreakpointValue } from 'native-base'; +import ScaffoldDesktopCC from '../components/UI/ScaffoldDesktopCC'; + +type Props = DefaultNavigatorOptions< + ParamListBase, + TabNavigationState, + BottomTabNavigationOptions, + BottomTabNavigationEventMap +> & + TabRouterOptions & + BottomTabNavigationConfig & { layout?: unknown }; + +function BottomTabNavigator({ + id, + initialRouteName, + backBehavior, + children, + layout, + screenListeners, + screenOptions, + sceneContainerStyle, + ...rest +}: Props) { + const { state, descriptors, navigation, NavigationContent } = useNavigationBuilder< + TabNavigationState, + TabRouterOptions, + TabActionHelpers, + BottomTabNavigationOptions, + BottomTabNavigationEventMap + >(TabRouter, { + id, + initialRouteName, + backBehavior, + children, + // @ts-expect-error The layout property has been added after the last release of react-navigation. + layout, + screenListeners, + screenOptions, + }); + + const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); + const descriptor = descriptors[state.routes[state.index]!.key]!; + const header = + descriptor.options.header ?? + (({ layout, options }: BottomTabHeaderProps) => ( +
+ )); + const dimensions = SafeAreaProviderCompat.initialMetrics.frame; + + return ( + + {screenSize === 'small' ? ( + + ) : ( + + , + options: descriptor.options, + })} + style={[sceneContainerStyle, { backgroundColor: 'transparent' }]} + > + {descriptor.render()} + + + )} + + ); +} + +export const createCustomNavigator = createNavigatorFactory< + TabNavigationState, + BottomTabNavigationOptions, + BottomTabNavigationEventMap, + typeof BottomTabNavigator +>(BottomTabNavigator); diff --git a/front/views/ArtistDetailsView.tsx b/front/views/ArtistDetailsView.tsx index 7f4ba84..92cd23f 100644 --- a/front/views/ArtistDetailsView.tsx +++ b/front/views/ArtistDetailsView.tsx @@ -5,7 +5,7 @@ import API from '../API'; import Song from '../models/Song'; import SongRow from '../components/SongRow'; import { Key } from 'react'; -import { RouteProps, useNavigation } from '../Navigation'; +import { useNavigation } from '../Navigation'; import { ImageBackground } from 'react-native'; import { useLikeSongMutation } from '../utils/likeSongMutation'; @@ -13,7 +13,7 @@ type ArtistDetailsViewProps = { artistId: number; }; -const ArtistDetailsView = ({ artistId }: RouteProps) => { +const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => { const artistQuery = useQuery(API.getArtist(artistId)); const songsQuery = useQuery(API.getSongsByArtist(artistId)); const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); diff --git a/front/views/ErrorView.tsx b/front/views/ErrorView.tsx index 3c5ec56..0b011c5 100644 --- a/front/views/ErrorView.tsx +++ b/front/views/ErrorView.tsx @@ -8,7 +8,7 @@ const ErrorView = () => {
- diff --git a/front/views/ForgotPasswordView.tsx b/front/views/ForgotPasswordView.tsx index 9b5e822..7bc30b4 100644 --- a/front/views/ForgotPasswordView.tsx +++ b/front/views/ForgotPasswordView.tsx @@ -12,7 +12,7 @@ const ForgotPasswordView = () => { route: `/auth/forgot-password?email=${email}`, method: 'PUT', }); - navigation.navigate('Home', {}); + navigation.navigate('Home'); return 'email sent'; } catch { return 'Error with email, please contact support'; diff --git a/front/views/GenreDetailsView.tsx b/front/views/GenreDetailsView.tsx index 64e77c3..62a964d 100644 --- a/front/views/GenreDetailsView.tsx +++ b/front/views/GenreDetailsView.tsx @@ -1,7 +1,7 @@ import { Flex, Heading, useBreakpointValue, ScrollView } from 'native-base'; import { useQuery } from '../Queries'; import { LoadingView } from '../components/Loading'; -import { RouteProps, useNavigation } from '../Navigation'; +import { useNavigation } from '../Navigation'; import API from '../API'; import CardGridCustom from '../components/CardGridCustom'; import SongCard from '../components/SongCard'; @@ -11,7 +11,7 @@ type GenreDetailsViewProps = { genreId: number; }; -const GenreDetailsView = ({ genreId }: RouteProps) => { +const GenreDetailsView = ({ genreId }: GenreDetailsViewProps) => { const genreQuery = useQuery(API.getGenre(genreId)); const songsQuery = useQuery(API.getSongsByGenre(genreId, ['artist'])); const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); diff --git a/front/views/HomeView.test.tsx b/front/views/HomeView.test.tsx deleted file mode 100644 index 59831a1..0000000 --- a/front/views/HomeView.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import TestRenderer from 'react-test-renderer'; -import store from '../state/Store'; - -import HomeView from '../views/HomeView'; - -describe('', () => { - it('has 2 children', () => { - const tree = TestRenderer.create( - - - - ).toJSON(); - expect(tree.children.length).toBe(2); - }); -}); diff --git a/front/views/HomeView.tsx b/front/views/HomeView.tsx deleted file mode 100644 index 795b8d1..0000000 --- a/front/views/HomeView.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import { useQuery } from '../Queries'; -import API from '../API'; -import { LoadingView } from '../components/Loading'; -import { Box, Flex, Stack, Heading, VStack, HStack } from 'native-base'; -import { RouteProps, useNavigation } from '../Navigation'; -import SongCardGrid from '../components/SongCardGrid'; -import CompetenciesTable from '../components/CompetenciesTable'; -import Translate from '../components/Translate'; -import TextButton from '../components/TextButton'; -import { FontAwesome5 } from '@expo/vector-icons'; -import ScaffoldCC from '../components/UI/ScaffoldCC'; - -// eslint-disable-next-line @typescript-eslint/ban-types -const HomeView = (props: RouteProps<{}>) => { - const navigation = useNavigation(); - const userQuery = useQuery(API.getUserInfo); - const playHistoryQuery = useQuery(API.getUserPlayHistory(['artist'])); - const searchHistoryQuery = useQuery(API.getSearchHistory(0, 10)); - const skillsQuery = useQuery(API.getUserSkills); - const nextStepQuery = useQuery(API.getSongSuggestions(['artist'])); - - if ( - !userQuery.data || - !skillsQuery.data || - !searchHistoryQuery.data || - !playHistoryQuery.data - ) { - return ; - } - return ( - - - - } - songs={ - nextStepQuery.data?.map((song) => ({ - cover: song.cover, - name: song.name, - songId: song.id, - artistName: song.artist!.name, - })) ?? [] - } - /> - - - - - - - - - - - } - songs={ - playHistoryQuery.data - ?.map((x) => x.song) - .map((song) => ({ - cover: song!.cover, - name: song!.name, - songId: song!.id, - artistName: song!.artist!.name, - })) ?? [] - } - /> - - - - - - navigation.navigate('Search', {})} - /> - navigation.navigate('Settings', {})} - /> - navigation.navigate('Leaderboard', {})} - /> - navigation.navigate('Home', {})} - /> - - - - - - - {searchHistoryQuery.data?.length === 0 && ( - - )} - {[...new Set(searchHistoryQuery.data.map((x) => x.query))] - .slice(0, 5) - .map((query) => ( - } - style={{ - margin: 2, - }} - key={query} - variant="solid" - size="xs" - colorScheme="primary" - label={query} - onPress={() => navigation.navigate('Search', {})} - /> - ))} - - - - - - ); -}; - -export default HomeView; diff --git a/front/views/LeaderboardView.tsx b/front/views/LeaderboardView.tsx index f4dc78f..fcb90d1 100644 --- a/front/views/LeaderboardView.tsx +++ b/front/views/LeaderboardView.tsx @@ -1,15 +1,14 @@ import { useBreakpointValue, ScrollView, Text } from 'native-base'; -import { SafeAreaView, View } from 'react-native'; +import { View } from 'react-native'; import { useQuery } from '../Queries'; import API from '../API'; import { LoadingView } from '../components/Loading'; -import { useNavigation, RouteProps } from '../Navigation'; -import ScaffoldCC from '../components/UI/ScaffoldCC'; +import { useNavigation } from '../Navigation'; import { translate } from '../i18n/i18n'; import { PodiumCard } from '../components/leaderboard/PodiumCard'; import { BoardRow } from '../components/leaderboard/BoardRow'; -const Leaderboardiew = (props: RouteProps>) => { +const Leaderboardiew = () => { const navigation = useNavigation(); const scoresQuery = useQuery(API.getTopTwentyPlayers()); const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); @@ -20,11 +19,7 @@ const Leaderboardiew = (props: RouteProps>) => { return <>; } if (!scoresQuery.data) { - return ( - - - - ); + return ; } const podiumUserData = [ @@ -34,140 +29,136 @@ const Leaderboardiew = (props: RouteProps>) => { ] as const; return ( - - - - + + {translate('leaderBoardHeading')} + + + {translate('leaderBoardHeadingFull')} + + + {!isPhone ? ( + - {translate('leaderBoardHeading')} - - - {translate('leaderBoardHeadingFull')} - + + + + + ) : ( - {!isPhone ? ( - - - - - - ) : ( - - - - - - - - )} + - {scoresQuery.data.slice(3, 20).map((comp, index) => ( - - ))} + + - - - + )} + + {scoresQuery.data.slice(3, 20).map((comp, index) => ( + + ))} + + + ); }; diff --git a/front/views/MusicView.tsx b/front/views/MusicView.tsx index adcbd18..3bfb4b4 100644 --- a/front/views/MusicView.tsx +++ b/front/views/MusicView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Center, useBreakpointValue, useTheme } from 'native-base'; +import { useBreakpointValue, useTheme } from 'native-base'; import { useWindowDimensions } from 'react-native'; import { TabView, @@ -11,105 +11,82 @@ import { } from 'react-native-tab-view'; import { Heart, Clock, StatusUp, FolderCross } from 'iconsax-react-native'; import { Scene } from 'react-native-tab-view/lib/typescript/src/types'; -import { RouteProps, useNavigation } from '../Navigation'; +import { useNavigation } from '../Navigation'; import { Translate, TranslationKey } from '../i18n/i18n'; -import ScaffoldCC from '../components/UI/ScaffoldCC'; import MusicList from '../components/UI/MusicList'; import { useQuery } from '../Queries'; import API from '../API'; import { LoadingView } from '../components/Loading'; import { useLikeSongMutation } from '../utils/likeSongMutation'; +import Song from '../models/Song'; -export const FavoritesMusic = () => { +type MusicListCCProps = { + data: Song[] | undefined; + isLoading: boolean; + refetch: () => void; +}; + +const MusicListCC = ({ data, isLoading, refetch }: MusicListCCProps) => { const navigation = useNavigation(); - const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory'])); const { mutateAsync } = useLikeSongMutation(); + const user = useQuery(API.getUserInfo); - const musics = - likedSongs.data?.map((x) => ({ - artist: x.song.artist!.name, - song: x.song.name, - image: x.song.cover, - lastScore: x.song.lastScore, - bestScore: x.song.bestScore, - liked: true, - onLike: () => { - mutateAsync({ songId: x.song.id, like: false }).then(() => likedSongs.refetch()); + const musics = (data ?? []).map((song) => { + const isLiked = song.likedByUsers?.some(({ userId }) => userId === user.data?.id) ?? false; + + return { + artist: song.artist!.name, + song: song.name, + image: song.cover, + lastScore: song.lastScore, + bestScore: song.bestScore, + liked: isLiked, + onLike: (state: boolean) => { + mutateAsync({ songId: song.id, like: state }).then(() => refetch()); }, - onPlay: () => navigation.navigate('Play', { songId: x.song.id }), - })) ?? []; + onPlay: () => navigation.navigate('Play', { songId: song.id }), + }; + }); - if (likedSongs.isLoading) { + if (isLoading) { return ; } + + return ; +}; + +const FavoritesMusic = () => { + const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory', 'likedByUsers'])); return ( - <> - {/* - console.log("A que coucou!")} - > - - Coucou - - - - */} - - + x.song)} + isLoading={likedSongs.isLoading} + refetch={likedSongs.refetch} + /> ); }; -export const RecentlyPlayedMusic = () => { +const RecentlyPlayedMusic = () => { + const playHistory = useQuery(API.getUserPlayHistory(['artist', 'SongHistory', 'likedByUsers'])); return ( -
- -
+ x.song !== undefined).map((x) => x.song) as Song[] + } + isLoading={playHistory.isLoading} + refetch={playHistory.refetch} + /> ); }; -export const StepUpMusic = () => { +const StepUpMusic = () => { + const nextStep = useQuery(API.getSongSuggestions(['artist', 'SongHistory', 'likedByUsers'])); return ( -
- -
+ ); }; @@ -132,7 +109,7 @@ const getTabData = (key: string) => { } }; -const MusicTab = (props: RouteProps) => { +const MusicTab = () => { const layout = useWindowDimensions(); const [index, setIndex] = React.useState(0); const { colors } = useTheme(); @@ -184,22 +161,20 @@ const MusicTab = (props: RouteProps) => { ); return ( - - - + ); }; diff --git a/front/views/PasswordResetView.tsx b/front/views/PasswordResetView.tsx index f7b3980..bd74b2e 100644 --- a/front/views/PasswordResetView.tsx +++ b/front/views/PasswordResetView.tsx @@ -17,7 +17,7 @@ const PasswordResetView = () => { password, }, }); - navigation.navigate('Home', {}); + navigation.navigate('Home'); return 'password succesfully reset'; } catch { return 'password reset failed'; diff --git a/front/views/PlayView.tsx b/front/views/PlayView.tsx index 07bc20a..e4fb224 100644 --- a/front/views/PlayView.tsx +++ b/front/views/PlayView.tsx @@ -1,18 +1,9 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { StackActions } from '@react-navigation/native'; import React, { useEffect, useRef, useState } from 'react'; import { SafeAreaView, Platform } from 'react-native'; -import Animated, { - useSharedValue, - withTiming, - Easing, - useAnimatedStyle, - withSequence, - withDelay, -} from 'react-native-reanimated'; import * as ScreenOrientation from 'expo-screen-orientation'; import { Text, Row, View, useToast } from 'native-base'; -import { RouteProps, useNavigation } from '../Navigation'; +import { useNavigation } from '../Navigation'; import { useQuery } from '../Queries'; import API from '../API'; import { LoadingView } from '../components/Loading'; @@ -33,16 +24,12 @@ import ButtonBase from '../components/UI/ButtonBase'; import { Clock, Cup } from 'iconsax-react-native'; import PlayViewControlBar from '../components/Play/PlayViewControlBar'; import ScoreModal from '../components/ScoreModal'; +import { PlayScore, ScoreMessage } from '../components/Play/PlayScore'; type PlayViewProps = { songId: number; }; -type ScoreMessage = { - content: string; - color?: ColorSchemeType; -}; - // this a hot fix this should be reverted soon let scoroBaseApiUrl = process.env.EXPO_PUBLIC_SCORO_URL!; @@ -68,7 +55,7 @@ function parseMidiMessage(message: MIDIMessageEvent) { }; } -const PlayView = ({ songId, route }: RouteProps) => { +const PlayView = ({ songId }: PlayViewProps) => { const [playType, setPlayType] = useState<'practice' | 'normal' | null>(null); const accessToken = useSelector((state: RootState) => state.user.accessToken); const navigation = useNavigation(); @@ -82,25 +69,18 @@ const PlayView = ({ songId, route }: RouteProps) => { const stopwatch = useStopwatch(); const [time, setTime] = useState(0); const [endResult, setEndResult] = useState(); + const [shouldPlay, setShouldPlay] = useState(false); const songHistory = useQuery(API.getSongHistory(songId)); const [score, setScore] = useState(0); // Between 0 and 100 - // const fadeAnim = useRef(new Animated.Value(0)).current; - const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000; + const getElapsedTime = () => stopwatch.getElapsedRunningTime(); + const [readyToPlay, setReadyToPlay] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [midiKeyboardFound, setMidiKeyboardFound] = useState(); // first number is the note, the other is the time when pressed on release the key is removed // eslint-disable-next-line @typescript-eslint/no-unused-vars const [streak, setStreak] = useState(0); - const scoreMessageScale = useSharedValue(0); - // this style should bounce in on enter and fade away - const scoreMsgStyle = useAnimatedStyle(() => { - return { - transform: [{ scale: scoreMessageScale.value }], - }; - }); const colorScheme = useColorScheme(); const { colors } = useTheme(); - const textColor = colors.text; const statColor = colors.lightText; const onPause = () => { @@ -130,11 +110,12 @@ const PlayView = ({ songId, route }: RouteProps) => { }) ); }; + const onEnd = () => { stopwatch.stop(); if (webSocket.current?.readyState != WebSocket.OPEN) { console.warn('onEnd: Websocket not open'); - navigation.dispatch(StackActions.replace('Home', {})); + navigation.replace('Tabs', { screen: 'Home' }); return; } webSocket.current?.send( @@ -226,7 +207,11 @@ const PlayView = ({ songId, route }: RouteProps) => { break; } } - setLastScoreMessage({ content: formattedMessage, color: messageColor }); + setLastScoreMessage({ + content: formattedMessage, + color: messageColor, + id: (lastScoreMessage?.id ?? 0) + 1, + }); } catch (e) { console.error(e); } @@ -263,27 +248,11 @@ const PlayView = ({ songId, route }: RouteProps) => { clearInterval(interval); }; }, []); - useEffect(() => { - if (lastScoreMessage) { - scoreMessageScale.value = withSequence( - withTiming(1, { - duration: 400, - easing: Easing.elastic(3), - }), - withDelay( - 700, - withTiming(0, { - duration: 300, - easing: Easing.out(Easing.cubic), - }) - ) - ); - } - }, [lastScoreMessage]); + useEffect(() => { // Song.data is updated on navigation.navigate (do not know why) // Hotfix to prevent midi setup process from reruning on game end - if (navigation.getState().routes.at(-1)?.name != route.name) { + if (navigation.getState().routes.at(-1)?.name != 'Play') { return; } if (playType && song.data && !webSocket.current) { @@ -362,12 +331,12 @@ const PlayView = ({ songId, route }: RouteProps) => { [ 'lastScore', songHistory.data?.history.at(0)?.score ?? 0, - () => , + , ] as const, [ 'bestScore', songHistory.data?.best ?? 0, - () => , + , ], ] as const ).map(([label, value, icon]) => ( @@ -388,7 +357,7 @@ const PlayView = ({ songId, route }: RouteProps) => { gap: 5, }} > - {icon()} + {icon} {value} @@ -402,48 +371,10 @@ const PlayView = ({ songId, route }: RouteProps) => { left: 0, zIndex: 100, width: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - gap: 3, position: 'absolute', }} > - - - {score} - - - - - - {lastScoreMessage?.content} - - - {streak > 0 && `x${streak}`} - - - + ) => { }} > { setTimeout(() => { onEnd(); - }, 500); + }, 200); }} onError={() => { console.log('error from partition magic'); }} + onReady={() => { + console.log('ready from partition magic'); + setReadyToPlay(true); + }} + onPlay={onResume} + onPause={onPause} /> { + setShouldPlay(false); + }} + onResume={() => { + setShouldPlay(true); + }} /> - { - if (!isVisible) { - // If we dismiss the popup, Go to previous page - navigation.goBack(); - } - } - : undefined - } - > - - setPlayType('practice')} - /> - setPlayType('normal')} - /> - - {colorScheme === 'dark' && ( ) => { +const ProfileView = () => { const layout = useWindowDimensions(); const navigation = useNavigation(); const userQuery = useQuery(API.getUserInfo); if (!userQuery.data) { - return ( - - - - ); + return ; } const progessValue = xpToProgressBarValue(userQuery.data.data.xp); @@ -40,89 +34,85 @@ const ProfileView = (props: RouteProps<{}>) => { const isBigScreen = layout.width > 650; return ( - - + + + + + - - - - - - {userQuery.data.name} - - navigation.navigate('Settings', {})} - /> - - - `${e} ${userQuery.data.data.createdAt.toLocaleDateString()}` - } + + {userQuery.data.name} + + navigation.navigate('Settings')} /> - - `${userQuery.data.data.gamesPlayed} ${e}`} - /> - - - - + `${e} ${level}`} + style={{ paddingBottom: 10, fontWeight: 'bold' }} + translationKey="accountCreatedOn" + format={(e) => `${e} ${userQuery.data.data.createdAt.toLocaleDateString()}`} /> - - - - - + + `${userQuery.data.data.gamesPlayed} ${e}`} + /> + + + + + `${e} ${level}`} + /> + + + + ); }; diff --git a/front/views/V2/DiscoveryView.tsx b/front/views/V2/DiscoveryView.tsx index d8d07a9..2d95be3 100644 --- a/front/views/V2/DiscoveryView.tsx +++ b/front/views/V2/DiscoveryView.tsx @@ -4,12 +4,10 @@ import React from 'react'; import { useQuery } from '../../Queries'; import SongCardInfo from '../../components/V2/SongCardInfo'; import API from '../../API'; -import { RouteProps, useNavigation } from '../../Navigation'; -import ScaffoldCC from '../../components/UI/ScaffoldCC'; +import { useNavigation } from '../../Navigation'; import GoldenRatio from '../../components/V2/GoldenRatio'; -// eslint-disable-next-line @typescript-eslint/ban-types -const HomeView = (props: RouteProps<{}>) => { +const HomeView = () => { const suggestionsQuery = useQuery( API.getSongSuggestions(['artist', 'likedByUsers', 'SongHistory']) ); @@ -20,71 +18,69 @@ const HomeView = (props: RouteProps<{}>) => { const suggestions = suggestionsQuery.data?.slice(4) ?? []; return ( - - + + + + + - - - + {'Suggestions'} + - - {'Suggestions'} - - - {suggestions.map((song) => ( - { - navigation.navigate('Play', { songId: song.id }); - }} - onPlay={() => { - console.log('play'); - }} - /> - ))} - + {suggestions.map((song) => ( + { + navigation.navigate('Play', { songId: song.id }); + }} + onPlay={() => { + console.log('play'); + }} + /> + ))} - - + + ); }; diff --git a/front/views/V2/SearchView.tsx b/front/views/V2/SearchView.tsx index cf1d537..712060c 100644 --- a/front/views/V2/SearchView.tsx +++ b/front/views/V2/SearchView.tsx @@ -1,13 +1,12 @@ import React, { useMemo } from 'react'; import { FlatList, HStack, useBreakpointValue, useTheme, Text, Row } from 'native-base'; -import { RouteProps, useNavigation } from '../../Navigation'; +import { useNavigation } from '../../Navigation'; import { ArrowRotateLeft, Cup } from 'iconsax-react-native'; import { View, StyleSheet } from 'react-native'; import { useQuery } from '../../Queries'; import { translate } from '../../i18n/i18n'; import SearchBarComponent from '../../components/V2/SearchBar'; import SearchHistory from '../../components/V2/SearchHistory'; -import ScaffoldCC from '../../components/UI/ScaffoldCC'; import MusicItem from '../../components/UI/MusicItem'; import API from '../../API'; import LoadingComponent from '../../components/Loading'; @@ -90,7 +89,7 @@ const MusicListNoOpti = ({ list }: { list: any[] }) => { }; // eslint-disable-next-line @typescript-eslint/ban-types -const SearchView = (props: RouteProps<{}>) => { +const SearchView = () => { const navigation = useNavigation(); const artistsQuery = useQuery(API.getAllArtists()); const [searchQuery, setSearchQuery] = React.useState({} as searchProps); @@ -143,12 +142,10 @@ const SearchView = (props: RouteProps<{}>) => { } return ( - setSearchQuery(query)} /> {result.length != 0 ? : } - ); }; diff --git a/front/views/VerifiedView.tsx b/front/views/VerifiedView.tsx index 7e7e5ff..93ebe83 100644 --- a/front/views/VerifiedView.tsx +++ b/front/views/VerifiedView.tsx @@ -17,7 +17,7 @@ const VerifiedView = () => { route: `/auth/verify?token=${(route.params as any).token}`, method: 'PUT', }); - navigation.navigate('Home', {}); + navigation.navigate('Home'); } catch { setFailed(true); } diff --git a/front/views/settings/SettingsView.tsx b/front/views/settings/SettingsView.tsx index b15c09d..7eb0717 100644 --- a/front/views/settings/SettingsView.tsx +++ b/front/views/settings/SettingsView.tsx @@ -14,8 +14,6 @@ import { } from 'react-native-tab-view'; import { HeartEdit, UserEdit, SecurityUser, FolderCross } from 'iconsax-react-native'; import { Scene } from 'react-native-tab-view/lib/typescript/src/types'; -import { RouteProps } from '../../Navigation'; -import ScaffoldCC from '../../components/UI/ScaffoldCC'; import { translate } from '../../i18n/i18n'; const renderScene = SceneMap({ @@ -37,8 +35,7 @@ const getTabData = (key: string) => { } }; -// eslint-disable-next-line @typescript-eslint/ban-types -const SettingsTab = (props: RouteProps<{}>) => { +const SettingsTab = () => { const layout = useWindowDimensions(); const [index, setIndex] = React.useState(0); const { colors } = useTheme(); @@ -94,23 +91,21 @@ const SettingsTab = (props: RouteProps<{}>) => { ); return ( - - - + ); }; diff --git a/front/yarn.lock b/front/yarn.lock index 3b90603..a1276bb 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2644,6 +2644,15 @@ invariant "^2.2.4" nullthrows "^1.1.1" +"@react-navigation/bottom-tabs@^6.5.11": + version "6.5.11" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.11.tgz#b6e67a3aa19e60ed9c1139fa0253586c479832d5" + integrity sha512-CBN/NOdxnMvmjw+AJQI1kltOYaClTZmGec5pQ3ZNTPX86ytbIOylDIITKMfTgHZcIEFQDymx1SHeS++PIL3Szw== + dependencies: + "@react-navigation/elements" "^1.3.21" + color "^4.2.3" + warn-once "^0.1.0" + "@react-navigation/core@^6.4.10": version "6.4.10" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.10.tgz#0c52621968b35e3a75e189e823d3b9e3bad77aff" @@ -4646,11 +4655,27 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colord@^2.9.1: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" @@ -6966,6 +6991,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-async-function@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" @@ -10698,6 +10728,13 @@ simple-plist@^1.1.0: bplist-parser "0.3.1" plist "^3.0.5" +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -10727,6 +10764,11 @@ slugify@^1.3.4: resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b" integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw== +smplr@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/smplr/-/smplr-0.12.1.tgz#53895a6928c2a3093d72e639440ca4be0b480e54" + integrity sha512-GEHuuP2vCtgu7+HY2F0DEnIxB5JHfxsZt4lJd9i/zxiZz1eBHheiy9h5Th32XGkLGObm/MMCfVs8Fwz6N/BKMw== + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" diff --git a/shell.nix b/shell.nix index 0fa4181..d13a2b0 100644 --- a/shell.nix +++ b/shell.nix @@ -5,8 +5,9 @@ pkgs.mkShell { nodePackages.prisma nodePackages."@nestjs/cli" nodePackages.npm + nodePackages.eas-cli eslint_d - nodejs_16 + nodejs_18 yarn (python3.withPackages (ps: with ps; [requests mido])) pkg-config