merge main
@@ -7,6 +7,7 @@ import {
|
||||
Body,
|
||||
Delete,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
HttpCode,
|
||||
Put,
|
||||
InternalServerErrorException,
|
||||
@@ -74,6 +75,10 @@ export class AuthController {
|
||||
await this.settingsService.createUserSetting(user.id);
|
||||
await this.authService.sendVerifyMail(user);
|
||||
} catch (e) {
|
||||
// check if the error is a duplicate key error
|
||||
if (e.code === 'P2002') {
|
||||
throw new ConflictException('Username or email already taken');
|
||||
}
|
||||
console.error(e);
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { GoogleStrategy } from './google.strategy';
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '1h' },
|
||||
signOptions: { expiresIn: '365d' },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
|
||||
@@ -30,7 +30,7 @@ Register Duplicates
|
||||
# We can't use the `Register` keyword because it assert for success
|
||||
POST /auth/register {"username": "user-duplicate", "password": "pass", "email": "mail@kyoo.moe"}
|
||||
Output
|
||||
Integer response status 400
|
||||
Integer response status 409
|
||||
Login user-duplicate
|
||||
[Teardown] DELETE /auth/me
|
||||
|
||||
|
||||
2
front/.gitignore
vendored
@@ -14,5 +14,7 @@ yarn.error*
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
yarn-error.log
|
||||
|
||||
.idea/
|
||||
.expo
|
||||
14
front/API.ts
@@ -96,8 +96,16 @@ export default class API {
|
||||
});
|
||||
if (!handle || handle.emptyResponse) {
|
||||
if (!response.ok) {
|
||||
console.log(await response.json());
|
||||
throw new APIError(response.statusText, response.status);
|
||||
let responseMessage = response.statusText;
|
||||
try {
|
||||
const responseData = await response.json();
|
||||
console.log(responseData);
|
||||
if (responseData.message) responseMessage = responseData.message;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new APIError(response.statusText, response.status, 'unknownError');
|
||||
}
|
||||
throw new APIError(responseMessage, response.status, 'unknownError');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -109,7 +117,7 @@ export default class API {
|
||||
try {
|
||||
const jsonResponse = JSON.parse(body);
|
||||
if (!response.ok) {
|
||||
throw new APIError(response.statusText ?? body, response.status);
|
||||
throw new APIError(response.statusText ?? body, response.status, 'unknownError');
|
||||
}
|
||||
const validated = await handler.validator.validate(jsonResponse).catch((e) => {
|
||||
if (e instanceof yup.ValidationError) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import store, { persistor } from './state/Store';
|
||||
@@ -16,8 +16,16 @@ const queryClient = new QueryClient(QueryRules);
|
||||
|
||||
export default function App() {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
setTimeout(SplashScreen.hideAsync, 500);
|
||||
useFonts({ Lexend: require('./assets/fonts/Lexend-VariableFont_wght.ttf') });
|
||||
|
||||
const [fontsLoaded] = useFonts({
|
||||
Lexend: require('./assets/fonts/lexend.ttf'),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (fontsLoaded) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [fontsLoaded]);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
|
||||
118
front/Theme.tsx
@@ -31,49 +31,99 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
900: '#212956',
|
||||
},
|
||||
secondary: {
|
||||
50: '#d8ffff',
|
||||
100: '#acffff',
|
||||
200: '#7dffff',
|
||||
300: '#4dffff',
|
||||
400: '#28ffff',
|
||||
500: '#18e5e6',
|
||||
600: '#00b2b3',
|
||||
700: '#007f80',
|
||||
800: '#004d4e',
|
||||
900: '#001b1d',
|
||||
50: '#f7f3ff',
|
||||
100: '#f3edfe',
|
||||
200: '#e6d9fe',
|
||||
300: '#ae84fb',
|
||||
400: '#9d77e2',
|
||||
500: '#8b6ac9',
|
||||
600: '#8363bc',
|
||||
700: '#684f97',
|
||||
800: '#4e3b71',
|
||||
900: '#3d2e58',
|
||||
},
|
||||
error: {
|
||||
50: '#ffe2e9',
|
||||
100: '#ffb1bf',
|
||||
200: '#ff7f97',
|
||||
300: '#ff4d6d',
|
||||
400: '#fe1d43',
|
||||
500: '#e5062b',
|
||||
600: '#b30020',
|
||||
700: '#810017',
|
||||
800: '#4f000c',
|
||||
900: '#200004',
|
||||
50: '#f7f3ff',
|
||||
100: '#f3edfe',
|
||||
200: '#e6d9fe',
|
||||
300: '#ae84fb',
|
||||
400: '#9d77e2',
|
||||
500: '#8b6ac9',
|
||||
600: '#8363bc',
|
||||
700: '#684f97',
|
||||
800: '#4e3b71',
|
||||
900: '#3d2e58',
|
||||
},
|
||||
alert: {
|
||||
50: '#fff2f1',
|
||||
100: '#ffebea',
|
||||
200: '#ffd6d3',
|
||||
300: '#ff7a72',
|
||||
400: '#e66e67',
|
||||
500: '#cc625b',
|
||||
600: '#bf5c56',
|
||||
700: '#994944',
|
||||
800: '#733733',
|
||||
900: '#592b28',
|
||||
},
|
||||
notification: {
|
||||
50: '#ffe1e1',
|
||||
100: '#ffb1b1',
|
||||
200: '#ff7f7f',
|
||||
300: '#ff4c4c',
|
||||
400: '#ff1a1a',
|
||||
500: '#e60000',
|
||||
600: '#b40000',
|
||||
700: '#810000',
|
||||
800: '#500000',
|
||||
900: '#210000',
|
||||
50: '#fdfbec',
|
||||
100: '#fcf9e2',
|
||||
200: '#f8f3c3',
|
||||
300: '#ead93c',
|
||||
400: '#d3c336',
|
||||
500: '#bbae30',
|
||||
600: '#b0a32d',
|
||||
700: '#8c8224',
|
||||
800: '#69621b',
|
||||
900: '#524c15',
|
||||
},
|
||||
black: {
|
||||
50: '#e7e7e8',
|
||||
100: '#dbdbdc',
|
||||
200: '#b5b5b6',
|
||||
300: '#101014',
|
||||
400: '#0e0e12',
|
||||
500: '#0d0d10',
|
||||
600: '#0c0c0f',
|
||||
700: '#0a0a0c',
|
||||
800: '#070709',
|
||||
900: '#060607',
|
||||
},
|
||||
red: {
|
||||
50: '#fdedee',
|
||||
100: '#fce4e5',
|
||||
200: '#f9c7c9',
|
||||
300: '#ed4a51',
|
||||
400: '#d54349',
|
||||
500: '#be3b41',
|
||||
600: '#b2383d',
|
||||
700: '#8e2c31',
|
||||
800: '#6b2124',
|
||||
900: '#531a1c',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
variants: {
|
||||
solid: () => ({
|
||||
rounded: 'full',
|
||||
}),
|
||||
baseStyle: () => ({
|
||||
borderRadius: 'md',
|
||||
}),
|
||||
},
|
||||
Link: {
|
||||
defaultProps: {
|
||||
isUnderlined: false,
|
||||
},
|
||||
baseStyle: () => ({
|
||||
_text: {
|
||||
color: 'secondary.300',
|
||||
},
|
||||
_hover: {
|
||||
isUnderlined: true,
|
||||
_text: {
|
||||
color: 'secondary.400',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
})}
|
||||
|
||||
@@ -6,7 +6,7 @@ module.exports = {
|
||||
icon: './assets/icon.png',
|
||||
userInterfaceStyle: 'light',
|
||||
splash: {
|
||||
image: './assets/splashLogo.png',
|
||||
image: './assets/splash.png',
|
||||
resizeMode: 'contain',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
@@ -18,12 +18,6 @@ module.exports = {
|
||||
supportsTablet: true,
|
||||
},
|
||||
android: {
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/adaptive-icon.png',
|
||||
backgroundColor: '#FFFFFF',
|
||||
package: 'com.chromacase.chromacase',
|
||||
versionCode: 1,
|
||||
},
|
||||
package: 'build.apk',
|
||||
},
|
||||
web: {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splashLogo.png",
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "cover",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
@@ -19,10 +19,6 @@
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"package": "build.apk"
|
||||
},
|
||||
"web": {
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB |
BIN
front/assets/auth/guest_banner.png
Normal file
|
After Width: | Height: | Size: 657 KiB |
BIN
front/assets/auth/login_banner.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
front/assets/auth/register_banner.png
Normal file
|
After Width: | Height: | Size: 609 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 18 KiB |
BIN
front/assets/fonts/lexend.ttf
Normal file
BIN
front/assets/full_dark.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
front/assets/full_light.png
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
front/assets/icon.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 65 KiB |
BIN
front/assets/icon_dark.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
front/assets/icon_light.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 32 KiB |
BIN
front/assets/title_dark.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
front/assets/title_light.png
Normal file
|
After Width: | Height: | Size: 498 KiB |
@@ -16,7 +16,7 @@ import useColorScheme from '../hooks/colorScheme';
|
||||
type BigActionButtonProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
image: string;
|
||||
image?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
iconName?: string;
|
||||
// It is not possible to recover the type, the `Icon` parameter is `any` as well.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import PartitionView from './PartitionView';
|
||||
import PhaserCanvas from './PartitionVisualizer/PhaserCanvas';
|
||||
import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas';
|
||||
import { PianoCursorPosition } from '../models/PianoGame';
|
||||
|
||||
type PartitionCoordProps = {
|
||||
// The Buffer of the MusicXML file retreived from the API
|
||||
@@ -10,8 +10,6 @@ type PartitionCoordProps = {
|
||||
onEndReached: () => void;
|
||||
onResume: () => void;
|
||||
onPause: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const PartitionCoord = ({
|
||||
@@ -20,10 +18,9 @@ const PartitionCoord = ({
|
||||
onEndReached,
|
||||
onPause,
|
||||
onResume,
|
||||
timestamp,
|
||||
}: PartitionCoordProps) => {
|
||||
const [partitionData, setPartitionData] = React.useState<
|
||||
[string, PianoCursorPosition[]] | null
|
||||
[[number, number], string, PianoCursorPosition[]] | null
|
||||
>(null);
|
||||
|
||||
return (
|
||||
@@ -31,21 +28,21 @@ const PartitionCoord = ({
|
||||
{!partitionData && (
|
||||
<PartitionView
|
||||
file={file}
|
||||
onPartitionReady={(base64data, a) => {
|
||||
setPartitionData([base64data, a]);
|
||||
onPartitionReady={(dims, base64data, a) => {
|
||||
setPartitionData([dims, base64data, a]);
|
||||
onPartitionReady();
|
||||
}}
|
||||
onEndReached={() => {
|
||||
console.log('osmd end reached');
|
||||
}}
|
||||
timestamp={timestamp}
|
||||
timestamp={0}
|
||||
/>
|
||||
)}
|
||||
{partitionData && (
|
||||
<PhaserCanvas
|
||||
partitionB64={partitionData?.[0]}
|
||||
cursorPositions={partitionData?.[1]}
|
||||
timestamp={timestamp}
|
||||
partitionDims={partitionData?.[0]}
|
||||
partitionB64={partitionData?.[1]}
|
||||
cursorPositions={partitionData?.[2]}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
onEndReached={() => {
|
||||
|
||||
@@ -10,12 +10,15 @@ import {
|
||||
Note,
|
||||
} from 'opensheetmusicdisplay';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas';
|
||||
|
||||
import { PianoCursorPosition } from '../models/PianoGame';
|
||||
type PartitionViewProps = {
|
||||
// The Buffer of the MusicXML file retreived from the API
|
||||
file: string;
|
||||
onPartitionReady: (base64data: string, cursorInfos: PianoCursorPosition[]) => void;
|
||||
onPartitionReady: (
|
||||
dims: [number, number],
|
||||
base64data: string,
|
||||
cursorInfos: PianoCursorPosition[]
|
||||
) => void;
|
||||
onEndReached: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
@@ -97,7 +100,6 @@ const PartitionView = (props: PartitionViewProps) => {
|
||||
});
|
||||
_osmd.cursor.next();
|
||||
}
|
||||
// console.log('curPos', curPos);
|
||||
_osmd.cursor.reset();
|
||||
_osmd.cursor.hide();
|
||||
// console.log('timestamp cursor', _osmd.cursor.iterator.CurrentSourceTimestamp);
|
||||
@@ -110,12 +112,18 @@ const PartitionView = (props: PartitionViewProps) => {
|
||||
if (!osmdCanvas) {
|
||||
throw new Error('No canvas found');
|
||||
}
|
||||
let scale = osmdCanvas.width / parseFloat(osmdCanvas.style.width);
|
||||
if (Number.isNaN(scale)) {
|
||||
console.error('Scale is NaN setting it to 1');
|
||||
scale = 1;
|
||||
}
|
||||
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
|
||||
props.onPartitionReady(
|
||||
[osmdCanvas.width, osmdCanvas.height],
|
||||
osmdCanvas.toDataURL(),
|
||||
curPos.map((pos) => {
|
||||
return {
|
||||
x: pos.offset,
|
||||
x: pos.offset * scale,
|
||||
timing: pos.sNinfos.sNL,
|
||||
timestamp: pos.sNinfos.ts,
|
||||
notes: pos.notes,
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
// create a simple phaser effect with a canvas that can be easily imported as a react component
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import Phaser from 'phaser';
|
||||
import useColorScheme from '../../hooks/colorScheme';
|
||||
import { RootState, useSelector } from '../../state/Store';
|
||||
import { setSoundPlayer as setSPStore } from '../../state/SoundPlayerSlice';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { SplendidGrandPiano, CacheStorage } from 'smplr';
|
||||
import { Note } from 'opensheetmusicdisplay';
|
||||
import { handlePianoGameMsg } from './PianoGameUpdateFunctions';
|
||||
import { PianoCC } from '../../views/PlayView';
|
||||
import { PianoCanvasMsg, PianoCursorNote, PianoCursorPosition } from '../../models/PianoGame';
|
||||
|
||||
let globalTimestamp = 0;
|
||||
let globalPressedKeys: Map<number, number> = new Map();
|
||||
// the messages are consummed from the end and new messages should be added at the end
|
||||
let globalMessages: Array<PianoCanvasMsg> = [];
|
||||
const globalStatus: 'playing' | 'paused' | 'stopped' = 'playing';
|
||||
|
||||
const isValidSoundPlayer = (soundPlayer: SplendidGrandPiano | undefined) => {
|
||||
return soundPlayer && soundPlayer.loaded;
|
||||
};
|
||||
|
||||
const min = (a: number, b: number) => (a < b ? a : b);
|
||||
const max = (a: number, b: number) => (a > b ? a : b);
|
||||
|
||||
const myFindLast = <T,>(a: T[], p: (_: T, _2: number) => boolean) => {
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (p(a[i]!, i)) {
|
||||
@@ -48,17 +57,42 @@ const getPianoScene = (
|
||||
private cursorPositionsIdx = -1;
|
||||
private partition!: Phaser.GameObjects.Image;
|
||||
private cursor!: Phaser.GameObjects.Rectangle;
|
||||
private emitter!: Phaser.GameObjects.Particles.ParticleEmitter;
|
||||
private nbTextureToload!: number;
|
||||
create() {
|
||||
this.textures.addBase64(
|
||||
'star',
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAAApCAYAAACMeY82AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjYyNzNEOEU4NzUxMzExRTRCN0ZCODQ1QUJCREFFQzA4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjYyNzNEOEU5NzUxMzExRTRCN0ZCODQ1QUJCREFFQzA4Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NjI3M0Q4RTY3NTEzMTFFNEI3RkI4NDVBQkJEQUVDMDgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjI3M0Q4RTc3NTEzMTFFNEI3RkI4NDVBQkJEQUVDMDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4vDiTDAAAB4UlEQVR42uxagY2DMAw0VRdghayQFVghs3SF/1XaEdoVGIGswAj5OK0rkwfalx5wiY2smAik+OrYFxcAAWLABFQJazmAykCOEhZRx0sBYdLHS0VFk6omVU2qOwRktS1jwYY5QKSAslqEtNBWM0lVt4xUQERUmdrWSV+VZi27xVYZU1OimYyOmJTRDB58VVyE5BUJQYhJGZYGYzMH87nuqwuoRSRVdLyB5hcoWIZxjs9P2buT3LkI0PP+BKcQriEp2jjnwO0XjDFwUDFRouNXF5HoQlK0iwKDVw10/GzPdzBIoo1zBApF1prb57iUw/zQxkdkpY1rwNhoOQMDkhpt62wqw+ZisMTiOwNQqLu2VMWpxhggOcBbe/zwlTvKqTfZxDyJYyAAfEyPTTF2/1AcWj8Ye39fU98+geHlebBuvv4xX8Zal5WoCEGnvn1y/na5JQdz55aOkM3knRyS5xwpbcZ/BSG//2uVWTrB6uFOAjHjoT9GzIojZzlYdJaRQNcPW0cMby3OtRl32w950PbU2yAAiGPk22qL0rp4hOSlEkFAfvGi6RwenCXsLkLGfuUcDGKf/J2tIkTGv/9t/xaQxQDCzyPFJdU1AYns5kO3jKAPZhQQPctohHweIJK+D/kRYAAaWClvtE6otAAAAABJRU5ErkJggg=='
|
||||
);
|
||||
this.textures.addBase64('partition', partitionB64);
|
||||
this.cursorPositionsIdx = -1;
|
||||
// this is to prevent multiple initialisation of the scene
|
||||
this.nbTextureToload = 2;
|
||||
|
||||
this.cameras.main.setBackgroundColor(colorScheme === 'light' ? '#FFFFFF' : '#000000');
|
||||
this.textures.on('onload', () => {
|
||||
this.nbTextureToload--;
|
||||
if (this.nbTextureToload > 0) return;
|
||||
this.partition = this.add.image(0, 0, 'partition').setOrigin(0, 0);
|
||||
this.cameras.main.setBounds(0, 0, this.partition.width, this.partition.height);
|
||||
|
||||
this.cursor = this.add.rectangle(0, 0, 30, 350, 0x31ef8c, 0.5).setOrigin(0, 0);
|
||||
const dims = this.partition.getBounds();
|
||||
// base ref normal cursor is 276px by 30px
|
||||
this.cursor = this.add
|
||||
.rectangle(0, 0, (dims.height * 30) / 276, dims.height, 0x31ef8c, 0.5)
|
||||
.setOrigin(0, 0);
|
||||
this.cameras.main.startFollow(this.cursor, true, 0.05, 0.05);
|
||||
|
||||
this.emitter = this.add.particles(0, 0, 'star', {
|
||||
lifespan: 700,
|
||||
duration: 100,
|
||||
follow: this.cursor,
|
||||
speed: { min: 10, max: 20 },
|
||||
scale: { start: 0, end: 0.4 },
|
||||
emitZone: { type: 'edge', source: this.cursor.getBounds(), quantity: 50 },
|
||||
|
||||
emitting: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +110,16 @@ const getPianoScene = (
|
||||
this.cursorPositionsIdx = idx;
|
||||
return true;
|
||||
}
|
||||
if (globalPressedKeys.size > 0) {
|
||||
this.cursor.fillAlpha = 0.9;
|
||||
} else if (this.cursor) {
|
||||
this.cursor.fillAlpha = 0.5;
|
||||
}
|
||||
|
||||
if (globalMessages.length > 0) {
|
||||
handlePianoGameMsg(globalMessages, this.emitter, undefined);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
if (cP) {
|
||||
@@ -100,47 +144,30 @@ const getPianoScene = (
|
||||
return PianoScene;
|
||||
};
|
||||
|
||||
type PianoCursorNote = {
|
||||
note: Note;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export type PianoCursorPosition = {
|
||||
// offset in pixels
|
||||
x: number;
|
||||
// timestamp in ms
|
||||
timing: number;
|
||||
timestamp: number;
|
||||
notes: PianoCursorNote[];
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
currentTimestamp: number;
|
||||
status: 'playing' | 'paused' | 'stopped';
|
||||
};
|
||||
|
||||
export type PhaserCanvasProps = {
|
||||
partitionDims: [number, number];
|
||||
partitionB64: string;
|
||||
cursorPositions: PianoCursorPosition[];
|
||||
onEndReached: () => void;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const PhaserCanvas = ({
|
||||
partitionDims,
|
||||
partitionB64,
|
||||
cursorPositions,
|
||||
onEndReached,
|
||||
timestamp,
|
||||
}: PhaserCanvasProps) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const dispatch = useDispatch();
|
||||
const pianoCC = useContext(PianoCC);
|
||||
const soundPlayer = useSelector((state: RootState) => state.soundPlayer.soundPlayer);
|
||||
const [game, setGame] = React.useState<Phaser.Game | null>(null);
|
||||
|
||||
globalTimestamp = timestamp;
|
||||
globalTimestamp = pianoCC.timestamp;
|
||||
globalPressedKeys = pianoCC.pressedKeys;
|
||||
globalMessages = pianoCC.messages;
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidSoundPlayer(soundPlayer)) {
|
||||
@@ -164,13 +191,43 @@ const PhaserCanvas = ({
|
||||
soundPlayer,
|
||||
colorScheme
|
||||
);
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
class UIScene extends Phaser.Scene {
|
||||
private statusTextValue: string;
|
||||
private statusText!: Phaser.GameObjects.Text;
|
||||
constructor() {
|
||||
super({ key: 'UIScene', active: true });
|
||||
|
||||
this.statusTextValue = 'Score: 0 Streak: 0';
|
||||
}
|
||||
|
||||
create() {
|
||||
this.statusText = this.add.text(
|
||||
this.cameras.main.width - 300,
|
||||
10,
|
||||
this.statusTextValue,
|
||||
{
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 25,
|
||||
color: '#3A784B',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override update() {
|
||||
if (globalMessages.length > 0) {
|
||||
handlePianoGameMsg(globalMessages, undefined, this.statusText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
type: Phaser.AUTO,
|
||||
parent: 'phaser-canvas',
|
||||
width: 1000,
|
||||
height: 400,
|
||||
scene: pianoScene,
|
||||
width: max(width * 0.9, 850),
|
||||
height: min(max(height * 0.7, 400), partitionDims[1]),
|
||||
scene: [pianoScene, UIScene],
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_HORIZONTALLY,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NoteTiming, PianoCanvasMsg, PianoScoreInfo } from '../../models/PianoGame';
|
||||
|
||||
const handleNoteTimingMsg = (
|
||||
noteTiming: NoteTiming,
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter
|
||||
) => {
|
||||
if (noteTiming === NoteTiming.Perfect) {
|
||||
// gold
|
||||
emitter.particleTint = 0xffd700;
|
||||
emitter.start(20);
|
||||
} else if (noteTiming === NoteTiming.Great) {
|
||||
emitter.particleTint = 0x00ffff;
|
||||
emitter.start(10);
|
||||
} else if (noteTiming === NoteTiming.Good) {
|
||||
// orange/brown
|
||||
emitter.particleTint = 0xffa500;
|
||||
emitter.start(5);
|
||||
} else if (noteTiming === NoteTiming.Missed) {
|
||||
emitter.particleTint = 0xff0000;
|
||||
emitter.start(5);
|
||||
} else if (noteTiming === NoteTiming.Wrong) {
|
||||
// maybe add some other effect
|
||||
}
|
||||
};
|
||||
|
||||
const handleScoreMsg = (score: PianoScoreInfo, statusText: Phaser.GameObjects.Text) => {
|
||||
statusText.setText(`Score: ${score.score} Streak: ${score.streak}`);
|
||||
};
|
||||
|
||||
const findAndRemove = <T>(arr: Array<T>, predicate: (el: T) => boolean): T | undefined => {
|
||||
const idx = arr.findIndex(predicate);
|
||||
if (idx === -1) {
|
||||
return undefined;
|
||||
}
|
||||
return arr.splice(idx, 1)[0];
|
||||
};
|
||||
|
||||
export const handlePianoGameMsg = (
|
||||
msgs: Array<PianoCanvasMsg>,
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter | undefined,
|
||||
statusText: Phaser.GameObjects.Text | undefined
|
||||
) => {
|
||||
// this is temporary way of hanlding messages it works ok but is laggy when
|
||||
// pressing a lot of keys in a short time I will be using phaser events in the future I think
|
||||
const msg = findAndRemove(msgs, (msg) => {
|
||||
if (emitter && msg.type === 'noteTiming') {
|
||||
return true;
|
||||
} else if (statusText && msg.type === 'scoreInfo') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (msg) {
|
||||
if (msg.type === 'noteTiming') {
|
||||
handleNoteTimingMsg(msg.data as NoteTiming, emitter!);
|
||||
} else if (msg.type === 'scoreInfo') {
|
||||
handleScoreMsg(msg.data as PianoScoreInfo, statusText!);
|
||||
}
|
||||
}
|
||||
};
|
||||
45
front/models/PianoGame.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Note } from 'opensheetmusicdisplay';
|
||||
|
||||
export type PianoCursorNote = {
|
||||
note: Note;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export type PianoCursorPosition = {
|
||||
// offset in pixels
|
||||
x: number;
|
||||
// timestamp in ms
|
||||
timing: number;
|
||||
timestamp: number;
|
||||
notes: PianoCursorNote[];
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
currentTimestamp: number;
|
||||
status: 'playing' | 'paused' | 'stopped';
|
||||
};
|
||||
|
||||
export type PianoScoreInfo = {
|
||||
score: number;
|
||||
streak: number;
|
||||
};
|
||||
|
||||
export enum NoteTiming {
|
||||
Perfect = 'Perfect',
|
||||
Great = 'Great',
|
||||
Good = 'Good',
|
||||
Missed = 'Missed',
|
||||
Wrong = 'Wrong',
|
||||
}
|
||||
|
||||
export type PianoCanvasMsg = {
|
||||
type: 'noteTiming' | 'scoreInfo' | 'gameUpdate';
|
||||
data: UpdateInfo | NoteTiming | PianoScoreInfo | number;
|
||||
};
|
||||
|
||||
export type PianoCanvasContext = {
|
||||
messages: Array<PianoCanvasMsg>;
|
||||
// Timestamp of the play session, in miliseconds
|
||||
timestamp: number;
|
||||
pressedKeys: Map<number, number>;
|
||||
};
|
||||
@@ -7,8 +7,8 @@ export const UserValidator = yup
|
||||
.object({
|
||||
username: yup.string().required(),
|
||||
password: yup.string().required().nullable(),
|
||||
email: yup.string().required(),
|
||||
emailVerified: yup.boolean().required(),
|
||||
email: yup.string().required().nullable(),
|
||||
googleID: yup.string().required().nullable(),
|
||||
isGuest: yup.boolean().required(),
|
||||
partyPlayed: yup.number().required(),
|
||||
@@ -32,8 +32,9 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
|
||||
|
||||
interface User extends Model {
|
||||
name: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
// guest accounts don't have a mail
|
||||
email: string | null;
|
||||
googleID: string | null;
|
||||
isGuest: boolean;
|
||||
premium: boolean;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10
|
||||
FROM python:3.10-alpine
|
||||
RUN wget -q -O /tmp/websocketd.zip \
|
||||
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
|
||||
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10
|
||||
FROM python:3.10-alpine
|
||||
RUN wget -q -O /tmp/websocketd.zip \
|
||||
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
|
||||
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \
|
||||
|
||||
@@ -58,7 +58,8 @@ class Scorometer:
|
||||
def __init__(self, mode: int, midiFile: str, song_id: int, user_id: int) -> None:
|
||||
self.partition: Partition = getPartition(midiFile)
|
||||
self.practice_partition: list[list[Key]] = self.getPracticePartition(mode)
|
||||
logging.debug({"partition": self.partition.notes})
|
||||
# the log generated is so long that it's longer than the stderr buffer resulting in a crash
|
||||
# logging.debug({"partition": self.partition.notes})
|
||||
self.keys_down = []
|
||||
self.mode: int = mode
|
||||
self.song_id: int = song_id
|
||||
|
||||