merge main

This commit is contained in:
mathysPaul
2023-09-19 19:22:25 +02:00
40 changed files with 455 additions and 215 deletions
+4 -1
View File
@@ -37,7 +37,10 @@ const handleSignup = async (
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof APIError) {
if (error.status === 409) return translate('usernameTaken');
return translate(error.userMessage);
}
if (error instanceof Error) return error.message;
return translate('unknownError');
}
+76 -17
View File
@@ -1,6 +1,6 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import { StackActions } from '@react-navigation/native';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, createContext, useReducer } from 'react';
import { SafeAreaView, Platform, Animated } from 'react-native';
import * as ScreenOrientation from 'expo-screen-orientation';
import {
@@ -32,6 +32,7 @@ import TextButton from '../components/TextButton';
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
import * as Linking from 'expo-linking';
import url from 'url';
import { PianoCanvasContext, PianoCanvasMsg, NoteTiming } from '../models/PianoGame';
type PlayViewProps = {
songId: number;
@@ -68,6 +69,13 @@ function parseMidiMessage(message: MIDIMessageEvent) {
};
}
//create a context with an array of number
export const PianoCC = createContext<PianoCanvasContext>({
pressedKeys: new Map(),
timestamp: 0,
messages: [],
});
const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const accessToken = useSelector((state: RootState) => state.user.accessToken);
const navigation = useNavigation();
@@ -87,6 +95,15 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
);
const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000;
const [midiKeyboardFound, setMidiKeyboardFound] = useState<boolean>();
// first number is the note, the other is the time when pressed on release the key is removed
const [pressedKeys, setPressedKeys] = useState<Map<number, number>>(new Map()); // [note, time]
const [pianoMsgs, setPianoMsgs] = useReducer(
(state: PianoCanvasMsg[], action: PianoCanvasMsg) => {
state.push(action);
return state;
},
[]
);
const onPause = () => {
stopwatch.pause();
@@ -137,7 +154,6 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
return;
}
setMidiKeyboardFound(true);
let inputIndex = 0;
webSocket.current = new WebSocket(scoroBaseApiUrl);
webSocket.current.onopen = () => {
webSocket.current!.send(
@@ -170,9 +186,18 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
);
return;
}
const currentStreak = data.info.current_streak;
const points = data.info.score;
const maxPoints = data.info.max_score || 1;
if (currentStreak !== undefined && points !== undefined) {
setPianoMsgs({
type: 'scoreInfo',
data: { streak: currentStreak, score: points },
});
}
setScore(Math.floor((Math.max(points, 0) * 100) / maxPoints));
let formattedMessage = '';
@@ -180,25 +205,45 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
if (data.type == 'miss') {
formattedMessage = translate('missed');
setPianoMsgs({
type: 'noteTiming',
data: NoteTiming.Missed,
});
messageColor = 'black';
} else if (data.type == 'timing' || data.type == 'duration') {
formattedMessage = translate(data[data.type]);
switch (data[data.type]) {
case 'perfect':
messageColor = 'green';
setPianoMsgs({
type: 'noteTiming',
data: NoteTiming.Perfect,
});
break;
case 'great':
messageColor = 'blue';
setPianoMsgs({
type: 'noteTiming',
data: NoteTiming.Great,
});
break;
case 'short':
case 'long':
case 'good':
messageColor = 'lightBlue';
setPianoMsgs({
type: 'noteTiming',
data: NoteTiming.Good,
});
break;
case 'too short':
case 'too long':
case 'wrong':
messageColor = 'trueGray';
setPianoMsgs({
type: 'noteTiming',
data: NoteTiming.Wrong,
});
break;
default:
break;
@@ -210,23 +255,30 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
}
};
inputs.forEach((input) => {
if (inputIndex != 0) {
return;
}
input.onmidimessage = (message) => {
const { command } = parseMidiMessage(message);
const { command, note } = parseMidiMessage(message);
const keyIsPressed = command == 9;
const keyCode = message.data[1];
if (keyIsPressed) {
setPressedKeys((prev) => {
prev.set(note, getElapsedTime());
return prev;
});
} else {
setPressedKeys((prev) => {
prev.delete(note);
return prev;
});
}
webSocket.current?.send(
JSON.stringify({
type: keyIsPressed ? 'note_on' : 'note_off',
note: keyCode,
note: note,
id: song.data!.id,
time: getElapsedTime(),
})
);
};
inputIndex++;
});
};
const onMIDIFailure = () => {
@@ -287,14 +339,21 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
</Animated.View>
</HStack>
<View style={{ flexGrow: 1, justifyContent: 'center' }}>
<PartitionCoord
file={musixml.data}
timestamp={time}
onEndReached={onEnd}
onPause={onPause}
onResume={onResume}
onPartitionReady={() => setPartitionRendered(true)}
/>
<PianoCC.Provider
value={{
pressedKeys: pressedKeys,
timestamp: time,
messages: pianoMsgs,
}}
>
<PartitionCoord
file={musixml.data}
onEndReached={onEnd}
onPause={onPause}
onResume={onResume}
onPartitionReady={() => setPartitionRendered(true)}
/>
</PianoCC.Provider>
{!partitionRendered && <LoadingComponent />}
</View>
+18 -33
View File
@@ -1,46 +1,31 @@
import React from 'react';
import { Dimensions, View } from 'react-native';
import { Box, Image, Heading, HStack } from 'native-base';
import { View } from 'react-native';
import { Box, Heading, HStack } from 'native-base';
import { useNavigation } from '../Navigation';
import TextButton from '../components/TextButton';
import UserAvatar from '../components/UserAvatar';
const ProfilePictureBannerAndLevel = () => {
const username = 'Username';
const level = '1';
// banner size
const dimensions = Dimensions.get('window');
const imageHeight = dimensions.height / 5;
const imageWidth = dimensions.width;
// need to change the padding for the username and level
return (
<View style={{ flexDirection: 'row' }}>
<Image
source={{ uri: 'https://wallpaperaccess.com/full/317501.jpg' }}
size="lg"
style={{ height: imageHeight, width: imageWidth, zIndex: 0, opacity: 0.5 }}
/>
<HStack zIndex={1} space={3} position={'absolute'} marginY={10} marginX={10}>
<UserAvatar size="lg" />
<Box>
<Heading>{username}</Heading>
<Heading>Level : {level}</Heading>
</Box>
</HStack>
</View>
);
};
import { LoadingView } from '../components/Loading';
import { useQuery } from '../Queries';
import API from '../API';
const ProfileView = () => {
const navigation = useNavigation();
const userQuery = useQuery(API.getUserInfo);
if (!userQuery.data) {
return <LoadingView />;
}
return (
<View style={{ flexDirection: 'column' }}>
<ProfilePictureBannerAndLevel />
<Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50} zIndex={1}>
<HStack space={3} marginY={10} marginX={10}>
<UserAvatar size="lg" />
<Box>
<Heading>{userQuery.data.name}</Heading>
<Heading>XP : {userQuery.data.data.xp}</Heading>
</Box>
</HStack>
<Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50}>
<TextButton
onPress={() => navigation.navigate('Settings')}
translate={{ translationKey: 'settingsBtn' }}
+20 -59
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import React from 'react';
import { useNavigation } from '../Navigation';
import {
@@ -6,7 +7,6 @@ import {
Stack,
Box,
useToast,
AspectRatio,
Column,
useBreakpointValue,
Image,
@@ -22,6 +22,8 @@ import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { useDispatch } from '../state/Store';
import { translate } from '../i18n/i18n';
import useColorScheme from '../hooks/colorScheme';
import { useAssets } from 'expo-asset';
const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount();
@@ -29,25 +31,21 @@ const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promi
return translate('loggedIn');
};
const imgLogin =
'https://media.discordapp.net/attachments/717080637038788731/1095980610981478470/Octopus_a_moder_style_image_of_a_musician_showing_a_member_card_c0b9072c-d834-40d5-bc83-796501e1382c.png?width=657&height=657';
const imgGuest =
'https://media.discordapp.net/attachments/717080637038788731/1095996800835539014/Chromacase_guest_2.png?width=865&height=657';
const imgRegister =
'https://media.discordapp.net/attachments/717080637038788731/1095991220267929641/chromacase_register.png?width=1440&height=511';
const imgBanner =
'https://chromacase.studio/wp-content/uploads/2023/03/music-sheet-music-color-2462438.jpg';
const imgLogo =
'https://chromacase.studio/wp-content/uploads/2023/03/cropped-cropped-splashLogo-280x300.png';
const StartPageView = () => {
const navigation = useNavigation();
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isSmallScreen = screenSize === 'small';
const dispatch = useDispatch();
const colorScheme = useColorScheme();
const toast = useToast();
const [icon] = useAssets(
colorScheme == 'light'
? require('../assets/icon_light.png')
: require('../assets/icon_dark.png')
);
const [loginBanner] = useAssets(require('../assets/auth/login_banner.png'));
const [guestBanner] = useAssets(require('../assets/auth/guest_banner.png'));
const [registerBanner] = useAssets(require('../assets/auth/register_banner.png'));
return (
<View
@@ -63,14 +61,14 @@ const StartPageView = () => {
justifyContent: 'center',
marginTop: 20,
}}
space={3}
>
<Icon
as={
<Image
alt="Chromacase logo"
source={{
uri: imgLogo,
}}
// source={{ uri: titleBanner?.at(0)?.uri }}
source={{ uri: icon?.at(0)?.uri }}
/>
}
size={isSmallScreen ? '5xl' : '6xl'}
@@ -89,7 +87,7 @@ const StartPageView = () => {
<BigActionButton
title="Authenticate"
subtitle="Save and resume your learning at anytime on all devices"
image={imgLogin}
image={loginBanner?.at(0)?.uri}
iconName="user"
iconProvider={FontAwesome5}
onPress={() => navigation.navigate('Login', {})}
@@ -102,7 +100,7 @@ const StartPageView = () => {
<BigActionButton
title="Test Chromacase"
subtitle="Use a guest account to see around but your progression won't be saved"
image={imgGuest}
image={guestBanner?.at(0)?.uri}
iconName="user-clock"
iconProvider={FontAwesome5}
onPress={() => {
@@ -128,7 +126,7 @@ const StartPageView = () => {
<Center>
<BigActionButton
title="Register"
image={imgRegister}
image={registerBanner?.at(0)?.uri}
subtitle="Create an account to save your progress"
iconProvider={FontAwesome5}
iconName="user-plus"
@@ -175,45 +173,8 @@ const StartPageView = () => {
alignItems: 'center',
}}
>
<Link
href="https://chromacase.studio"
isExternal
style={{
width: 'clamp(200px, 100%, 700px)',
position: 'relative',
overflow: 'hidden',
borderRadius: 10,
}}
>
<AspectRatio ratio={40 / 9} style={{ width: '100%' }}>
<Image
alt="Chromacase Banner"
source={{ uri: imgBanner }}
resizeMode="cover"
/>
</AspectRatio>
<Box
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)',
}}
></Box>
<Heading
fontSize="2xl"
style={{
textAlign: 'center',
position: 'absolute',
top: '40%',
left: 20,
color: 'white',
}}
>
Click here for more infos
</Heading>
<Link href="http://eip.epitech.eu/2024/chromacase" isExternal>
Click here for more info
</Link>
</Box>
</Box>