diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index bc48297..b63d71c 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -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(); } diff --git a/back/src/auth/auth.module.ts b/back/src/auth/auth.module.ts index c5fee1f..137eb96 100644 --- a/back/src/auth/auth.module.ts +++ b/back/src/auth/auth.module.ts @@ -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], }), diff --git a/back/test/robot/auth/auth.robot b/back/test/robot/auth/auth.robot index 08a7273..7d39b97 100644 --- a/back/test/robot/auth/auth.robot +++ b/back/test/robot/auth/auth.robot @@ -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 diff --git a/front/.gitignore b/front/.gitignore index 80e998a..300084a 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -14,5 +14,7 @@ yarn.error* # macOS .DS_Store +yarn-error.log + .idea/ .expo \ No newline at end of file diff --git a/front/API.ts b/front/API.ts index 37184d3..4d97bde 100644 --- a/front/API.ts +++ b/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) { diff --git a/front/App.tsx b/front/App.tsx index 6bee3ff..415265b 100644 --- a/front/App.tsx +++ b/front/App.tsx @@ -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 ( diff --git a/front/Theme.tsx b/front/Theme.tsx index 8e02c3d..27cf7fe 100644 --- a/front/Theme.tsx +++ b/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', + }, + }, + }), }, }, })} diff --git a/front/app.config.ts b/front/app.config.ts index e9cd5e5..a556143 100644 --- a/front/app.config.ts +++ b/front/app.config.ts @@ -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: { diff --git a/front/app.json b/front/app.json index 26b3474..2045f9a 100644 --- a/front/app.json +++ b/front/app.json @@ -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": { diff --git a/front/assets/adaptive-icon.png b/front/assets/adaptive-icon.png deleted file mode 100644 index 03d6f6b..0000000 Binary files a/front/assets/adaptive-icon.png and /dev/null differ diff --git a/front/assets/auth/guest_banner.png b/front/assets/auth/guest_banner.png new file mode 100644 index 0000000..028afc3 Binary files /dev/null and b/front/assets/auth/guest_banner.png differ diff --git a/front/assets/auth/login_banner.png b/front/assets/auth/login_banner.png new file mode 100644 index 0000000..e75c36a Binary files /dev/null and b/front/assets/auth/login_banner.png differ diff --git a/front/assets/auth/register_banner.png b/front/assets/auth/register_banner.png new file mode 100644 index 0000000..94099b8 Binary files /dev/null and b/front/assets/auth/register_banner.png differ diff --git a/front/assets/cover.png b/front/assets/cover.png deleted file mode 100644 index 58bd287..0000000 Binary files a/front/assets/cover.png and /dev/null differ diff --git a/front/assets/favicon.png b/front/assets/favicon.png index e75f697..fb3074d 100644 Binary files a/front/assets/favicon.png and b/front/assets/favicon.png differ diff --git a/front/assets/fonts/lexend.ttf b/front/assets/fonts/lexend.ttf new file mode 100644 index 0000000..b294dc8 Binary files /dev/null and b/front/assets/fonts/lexend.ttf differ diff --git a/front/assets/full_dark.png b/front/assets/full_dark.png new file mode 100644 index 0000000..2e18a08 Binary files /dev/null and b/front/assets/full_dark.png differ diff --git a/front/assets/full_light.png b/front/assets/full_light.png new file mode 100644 index 0000000..8b39d83 Binary files /dev/null and b/front/assets/full_light.png differ diff --git a/front/assets/icon.jpg b/front/assets/icon.jpg new file mode 100644 index 0000000..9477e70 Binary files /dev/null and b/front/assets/icon.jpg differ diff --git a/front/assets/icon.png b/front/assets/icon.png index a0b1526..32cf37b 100644 Binary files a/front/assets/icon.png and b/front/assets/icon.png differ diff --git a/front/assets/icon_dark.png b/front/assets/icon_dark.png new file mode 100644 index 0000000..cf3233c Binary files /dev/null and b/front/assets/icon_dark.png differ diff --git a/front/assets/icon_light.png b/front/assets/icon_light.png new file mode 100644 index 0000000..ce13b12 Binary files /dev/null and b/front/assets/icon_light.png differ diff --git a/front/assets/splash.png b/front/assets/splash.png index 0e89705..bd948b0 100644 Binary files a/front/assets/splash.png and b/front/assets/splash.png differ diff --git a/front/assets/splashLogo.png b/front/assets/splashLogo.png deleted file mode 100644 index a7205cb..0000000 Binary files a/front/assets/splashLogo.png and /dev/null differ diff --git a/front/assets/title_dark.png b/front/assets/title_dark.png new file mode 100644 index 0000000..38c7b97 Binary files /dev/null and b/front/assets/title_dark.png differ diff --git a/front/assets/title_light.png b/front/assets/title_light.png new file mode 100644 index 0000000..4946a55 Binary files /dev/null and b/front/assets/title_light.png differ diff --git a/front/components/BigActionButton.tsx b/front/components/BigActionButton.tsx index 847c59d..b268972 100644 --- a/front/components/BigActionButton.tsx +++ b/front/components/BigActionButton.tsx @@ -16,7 +16,7 @@ import useColorScheme from '../hooks/colorScheme'; type BigActionButtonProps = { title: string; subtitle: string; - image: string; + image?: string; style?: StyleProp; iconName?: string; // It is not possible to recover the type, the `Icon` parameter is `any` as well. diff --git a/front/components/PartitionCoord.tsx b/front/components/PartitionCoord.tsx index 210382e..60fbace 100644 --- a/front/components/PartitionCoord.tsx +++ b/front/components/PartitionCoord.tsx @@ -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 && ( { - setPartitionData([base64data, a]); + onPartitionReady={(dims, base64data, a) => { + setPartitionData([dims, base64data, a]); onPartitionReady(); }} onEndReached={() => { console.log('osmd end reached'); }} - timestamp={timestamp} + timestamp={0} /> )} {partitionData && ( { diff --git a/front/components/PartitionView.tsx b/front/components/PartitionView.tsx index e9a6ed1..8fb7784 100644 --- a/front/components/PartitionView.tsx +++ b/front/components/PartitionView.tsx @@ -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, diff --git a/front/components/PartitionVisualizer/PhaserCanvas.tsx b/front/components/PartitionVisualizer/PhaserCanvas.tsx index eb7b4fa..848ab41 100644 --- a/front/components/PartitionVisualizer/PhaserCanvas.tsx +++ b/front/components/PartitionVisualizer/PhaserCanvas.tsx @@ -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 = new Map(); +// the messages are consummed from the end and new messages should be added at the end +let globalMessages: Array = []; 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 = (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(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, diff --git a/front/components/PartitionVisualizer/PianoGameUpdateFunctions.ts b/front/components/PartitionVisualizer/PianoGameUpdateFunctions.ts new file mode 100644 index 0000000..621fccc --- /dev/null +++ b/front/components/PartitionVisualizer/PianoGameUpdateFunctions.ts @@ -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 = (arr: Array, 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, + 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!); + } + } +}; diff --git a/front/models/PianoGame.ts b/front/models/PianoGame.ts new file mode 100644 index 0000000..424c464 --- /dev/null +++ b/front/models/PianoGame.ts @@ -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; + // Timestamp of the play session, in miliseconds + timestamp: number; + pressedKeys: Map; +}; diff --git a/front/models/User.ts b/front/models/User.ts index a53a110..e4bc5a1 100644 --- a/front/models/User.ts +++ b/front/models/User.ts @@ -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, 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; diff --git a/front/views/AuthenticationView.tsx b/front/views/AuthenticationView.tsx index b50862d..36ff7b9 100644 --- a/front/views/AuthenticationView.tsx +++ b/front/views/AuthenticationView.tsx @@ -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'); } diff --git a/front/views/PlayView.tsx b/front/views/PlayView.tsx index 2648052..790a556 100644 --- a/front/views/PlayView.tsx +++ b/front/views/PlayView.tsx @@ -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({ + pressedKeys: new Map(), + timestamp: 0, + messages: [], +}); + const PlayView = ({ songId, type, route }: RouteProps) => { const accessToken = useSelector((state: RootState) => state.user.accessToken); const navigation = useNavigation(); @@ -87,6 +95,15 @@ const PlayView = ({ songId, type, route }: RouteProps) => { ); const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000; const [midiKeyboardFound, setMidiKeyboardFound] = useState(); + // first number is the note, the other is the time when pressed on release the key is removed + const [pressedKeys, setPressedKeys] = useState>(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) => { 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) => { ); 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) => { 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) => { } }; 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) => { - setPartitionRendered(true)} - /> + + setPartitionRendered(true)} + /> + {!partitionRendered && } diff --git a/front/views/ProfileView.tsx b/front/views/ProfileView.tsx index ec2a30a..004c5c1 100644 --- a/front/views/ProfileView.tsx +++ b/front/views/ProfileView.tsx @@ -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 ( - - - - - - {username} - Level : {level} - - - - ); -}; +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 ; + } return ( - - + + + + {userQuery.data.name} + XP : {userQuery.data.data.xp} + + + navigation.navigate('Settings')} translate={{ translationKey: 'settingsBtn' }} diff --git a/front/views/StartPageView.tsx b/front/views/StartPageView.tsx index 302ffcf..4fa631f 100644 --- a/front/views/StartPageView.tsx +++ b/front/views/StartPageView.tsx @@ -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 => { 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 ( { justifyContent: 'center', marginTop: 20, }} + space={3} > } size={isSmallScreen ? '5xl' : '6xl'} @@ -89,7 +87,7 @@ const StartPageView = () => { navigation.navigate('Login', {})} @@ -102,7 +100,7 @@ const StartPageView = () => { { @@ -128,7 +126,7 @@ const StartPageView = () => {
{ alignItems: 'center', }} > - - - Chromacase Banner - - - - Click here for more infos - + + Click here for more info diff --git a/scorometer/Dockerfile b/scorometer/Dockerfile index f7a2f45..e75a9e7 100644 --- a/scorometer/Dockerfile +++ b/scorometer/Dockerfile @@ -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 \ diff --git a/scorometer/Dockerfile.dev b/scorometer/Dockerfile.dev index 2075bd7..f8d6e87 100644 --- a/scorometer/Dockerfile.dev +++ b/scorometer/Dockerfile.dev @@ -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 \ diff --git a/scorometer/main.py b/scorometer/main.py index f13917e..adbb7f6 100755 --- a/scorometer/main.py +++ b/scorometer/main.py @@ -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