merge main
This commit is contained in:
@@ -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
@@ -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
@@ -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' }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user