Front: Merge
@@ -12,12 +12,13 @@
|
||||
"webpack.config.js",
|
||||
"babel.config.js",
|
||||
"*.test.*",
|
||||
"app.config.ts"
|
||||
"app.config.ts",
|
||||
"android/"
|
||||
],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-restricted-imports": [
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
.expo-shared/
|
||||
dist/
|
||||
.vscode/
|
||||
.storybook/
|
||||
.storybook/
|
||||
android/
|
||||
19
front/API.ts
@@ -65,11 +65,22 @@ export class ValidationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseAPIUrl() {
|
||||
if (Platform.OS === 'web') {
|
||||
if (__DEV__ && process.env.EXPO_PUBLIC_API_URL) {
|
||||
return process.env.EXPO_PUBLIC_API_URL;
|
||||
}
|
||||
return '/api';
|
||||
}
|
||||
if (process.env.EXPO_PUBLIC_API_URL) {
|
||||
return process.env.EXPO_PUBLIC_API_URL;
|
||||
}
|
||||
// fallback since some mobile build seems to not have the env variable
|
||||
return 'https://nightly.chroma.octohub.app/api';
|
||||
}
|
||||
|
||||
export default class API {
|
||||
public static readonly baseUrl =
|
||||
Platform.OS === 'web' && !process.env.EXPO_PUBLIC_API_URL
|
||||
? '/api'
|
||||
: process.env.EXPO_PUBLIC_API_URL!;
|
||||
public static readonly baseUrl = getBaseAPIUrl();
|
||||
public static async fetch(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams>, 'raw'>
|
||||
|
||||
@@ -1,35 +1,53 @@
|
||||
import { NativeBaseProvider, extendTheme, useColorMode } from 'native-base';
|
||||
import useColorScheme from './hooks/colorScheme';
|
||||
import { useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const defaultLightGlassmorphism = {
|
||||
50: 'rgba(255,255,255,0.9)',
|
||||
100: 'rgba(255,255,255,0.1)',
|
||||
200: 'rgba(255,255,255,0.2)',
|
||||
300: 'rgba(255,255,255,0.3)',
|
||||
400: 'rgba(255,255,255,0.4)',
|
||||
500: 'rgba(255,255,255,0.5)',
|
||||
600: 'rgba(255,255,255,0.6)',
|
||||
700: 'rgba(255,255,255,0.7)',
|
||||
800: 'rgba(255,255,255,0.8)',
|
||||
900: 'rgba(255,255,255,0.9)',
|
||||
1000: 'rgba(255,255,255,1)',
|
||||
};
|
||||
// shodws are hugely visible on phone so we trick the colors without alpha
|
||||
const phoneLightGlassmorphism = {
|
||||
50: 'rgb(200, 204, 254)',
|
||||
100: 'rgb(204, 208, 254)',
|
||||
200: 'rgb(210, 214, 254)',
|
||||
300: 'rgb(214, 218, 254)',
|
||||
400: 'rgb(220, 222, 254)',
|
||||
500: 'rgb(230, 234, 254)',
|
||||
600: 'rgb(234, 236, 254)',
|
||||
700: 'rgb(240, 242, 254)',
|
||||
800: 'rgb(244, 246, 254)',
|
||||
900: 'rgb(248, 250, 254)',
|
||||
1000: 'rgb(252, 254, 254)',
|
||||
};
|
||||
const lightGlassmorphism =
|
||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
||||
const darkGlassmorphism = {
|
||||
50: 'rgba(16,16,20,0.9)',
|
||||
100: 'rgba(16,16,20,0.1)',
|
||||
200: 'rgba(16,16,20,0.2)',
|
||||
300: 'rgba(16,16,20,0.3)',
|
||||
400: 'rgba(16,16,20,0.4)',
|
||||
500: 'rgba(16,16,20,0.5)',
|
||||
600: 'rgba(16,16,20,0.6)',
|
||||
700: 'rgba(16,16,20,0.7)',
|
||||
800: 'rgba(16,16,20,0.8)',
|
||||
900: 'rgba(16,16,20,0.9)',
|
||||
1000: 'rgba(16,16,20,1)',
|
||||
};
|
||||
|
||||
const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const lightGlassmorphism = {
|
||||
50: 'rgba(255,255,255,0.9)',
|
||||
100: 'rgba(255,255,255,0.1)',
|
||||
200: 'rgba(255,255,255,0.2)',
|
||||
300: 'rgba(255,255,255,0.3)',
|
||||
400: 'rgba(255,255,255,0.4)',
|
||||
500: 'rgba(255,255,255,0.5)',
|
||||
600: 'rgba(255,255,255,0.6)',
|
||||
700: 'rgba(255,255,255,0.7)',
|
||||
800: 'rgba(255,255,255,0.8)',
|
||||
900: 'rgba(255,255,255,0.9)',
|
||||
1000: 'rgba(255,255,255,1)',
|
||||
};
|
||||
const darkGlassmorphism = {
|
||||
50: 'rgba(16,16,20,0.9)',
|
||||
100: 'rgba(16,16,20,0.1)',
|
||||
200: 'rgba(16,16,20,0.2)',
|
||||
300: 'rgba(16,16,20,0.3)',
|
||||
400: 'rgba(16,16,20,0.4)',
|
||||
500: 'rgba(16,16,20,0.5)',
|
||||
600: 'rgba(16,16,20,0.6)',
|
||||
700: 'rgba(16,16,20,0.7)',
|
||||
800: 'rgba(16,16,20,0.8)',
|
||||
900: 'rgba(16,16,20,0.9)',
|
||||
1000: 'rgba(16,16,20,1)',
|
||||
};
|
||||
|
||||
const glassmorphism = colorScheme === 'light' ? lightGlassmorphism : darkGlassmorphism;
|
||||
const text = colorScheme === 'light' ? darkGlassmorphism : lightGlassmorphism;
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 31 KiB |
@@ -2,4 +2,5 @@
|
||||
<string name="app_name">Chromacase</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">cover</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||
</resources>
|
||||
@@ -13,7 +13,8 @@
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.arthichaud.Chromacase"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
@@ -38,6 +39,7 @@
|
||||
"photosPermission": "The app accesses your photos to let you set your personal avatar."
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
"owner": "arthi-chaud"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ const ElementList = ({ elements, style }: ElementListProps) => {
|
||||
borderRadius: 10,
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: 'transparent',
|
||||
overflow: 'hidden',
|
||||
} as const;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Slider, Text, View, IconButton, Icon } from 'native-base';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Audio } from 'expo-av';
|
||||
@@ -6,7 +6,7 @@ import { VolumeHigh, VolumeSlash } from 'iconsax-react-native';
|
||||
|
||||
export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; bpm: number }) => {
|
||||
const audio = useRef<Audio.Sound | null>(null);
|
||||
const enabled = useRef<boolean>(false);
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
const volume = useRef<number>(50);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,7 +25,7 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
const int = setInterval(() => {
|
||||
if (!enabled.current) return;
|
||||
if (!enabled) return;
|
||||
if (!audio.current) return;
|
||||
audio.current?.playAsync();
|
||||
}, 60000 / bpm);
|
||||
@@ -60,7 +60,7 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
|
||||
<VolumeHigh size={24} color="white" />
|
||||
)
|
||||
}
|
||||
onPress={() => (enabled.current = !enabled.current)}
|
||||
onPress={() => setEnabled(!enabled)}
|
||||
/>
|
||||
<Slider
|
||||
maxWidth={'500px'}
|
||||
|
||||
@@ -2,19 +2,20 @@ import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import API from '../../API';
|
||||
import { useQuery } from '../../Queries';
|
||||
import { PianoCC } from '../../views/PlayView';
|
||||
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
|
||||
import { CursorInfoItem } from '../../models/SongCursorInfos';
|
||||
import { PianoNotes } from '../../state/SoundPlayerSlice';
|
||||
import { Audio } from 'expo-av';
|
||||
import { SvgContainer } from './SvgContainer';
|
||||
import LoadingComponent from '../Loading';
|
||||
|
||||
// note we are also using timestamp in a context
|
||||
export type ParitionMagicProps = {
|
||||
timestamp: number;
|
||||
songID: number;
|
||||
onEndReached: () => void;
|
||||
onError: (err: string) => void;
|
||||
onReady: () => void;
|
||||
onError?: (err: string) => void;
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
const getSVGURL = (songID: number) => {
|
||||
@@ -38,16 +39,23 @@ const getCursorToPlay = (
|
||||
}
|
||||
};
|
||||
|
||||
const PartitionMagic = ({ songID, onEndReached, onError, onReady }: ParitionMagicProps) => {
|
||||
const PartitionMagic = ({
|
||||
timestamp,
|
||||
songID,
|
||||
onEndReached,
|
||||
onError,
|
||||
onReady,
|
||||
}: ParitionMagicProps) => {
|
||||
const { data, isLoading, isError } = useQuery(API.getSongCursorInfos(songID));
|
||||
const [currentCurIdx, setCurrentCurIdx] = React.useState(-1);
|
||||
const currentCurIdx = React.useRef(-1);
|
||||
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
||||
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
|
||||
const partitionOffset = useSharedValue(0);
|
||||
const pianoCC = React.useContext(PianoCC);
|
||||
const pianoSounds = React.useRef<Record<string, Audio.Sound>>();
|
||||
const pianoSounds = React.useRef<Record<string, Audio.Sound> | null>(null);
|
||||
const cursorPaddingVertical = 10;
|
||||
const cursorPaddingHorizontal = 3;
|
||||
|
||||
const cursorDisplayIdx = currentCurIdx === -1 ? 0 : currentCurIdx;
|
||||
const cursorDisplayIdx = currentCurIdx.current === -1 ? 0 : currentCurIdx.current;
|
||||
|
||||
const cursorBorderWidth = (data?.cursors[cursorDisplayIdx]?.width ?? 0) / 6;
|
||||
const cursorWidth = (data?.cursors[cursorDisplayIdx]?.width ?? 0) + cursorPaddingHorizontal * 2;
|
||||
@@ -55,6 +63,12 @@ const PartitionMagic = ({ songID, onEndReached, onError, onReady }: ParitionMagi
|
||||
const cursorTop = (data?.cursors[cursorDisplayIdx]?.y ?? 0) - cursorPaddingVertical;
|
||||
const cursorLeft = (data?.cursors[0]?.x ?? 0) - cursorPaddingHorizontal;
|
||||
|
||||
if (!endPartitionReached && currentCurIdx.current + 1 === data?.cursors.length) {
|
||||
// weird contraption but the mobile don't want classic functions to be called
|
||||
// with the withTiming function :(
|
||||
setEndPartitionReached(true);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pianoSounds.current) {
|
||||
Promise.all(
|
||||
@@ -64,13 +78,13 @@ const PartitionMagic = ({ songID, onEndReached, onError, onReady }: ParitionMagi
|
||||
progressUpdateIntervalMillis: 100,
|
||||
}).then((sound) => [midiNumber, sound.sound] as const)
|
||||
)
|
||||
).then(
|
||||
(res) =>
|
||||
(pianoSounds.current = res.reduce(
|
||||
(prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
|
||||
{}
|
||||
))
|
||||
);
|
||||
).then((res) => {
|
||||
pianoSounds.current = res.reduce(
|
||||
(prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
|
||||
{}
|
||||
);
|
||||
console.log('sound loaded');
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const partitionDims = React.useMemo<[number, number]>(() => {
|
||||
@@ -78,46 +92,52 @@ const PartitionMagic = ({ songID, onEndReached, onError, onReady }: ParitionMagi
|
||||
}, [data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
if (isError) {
|
||||
if (onError && isError) {
|
||||
onError('Error while loading partition');
|
||||
return;
|
||||
}
|
||||
}, [isLoading, isError]);
|
||||
}, [onError, isError]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onReady && isPartitionSvgLoaded && !isLoading) {
|
||||
onReady();
|
||||
}
|
||||
}, [onReady, isPartitionSvgLoaded, isLoading]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (endPartitionReached) {
|
||||
onEndReached();
|
||||
}
|
||||
}, [endPartitionReached]);
|
||||
|
||||
const transitionDuration = 200;
|
||||
|
||||
getCursorToPlay(
|
||||
data?.cursors ?? [],
|
||||
currentCurIdx,
|
||||
pianoCC.timestamp - transitionDuration,
|
||||
currentCurIdx.current,
|
||||
timestamp - transitionDuration,
|
||||
(cursor, idx) => {
|
||||
cursor.notes.forEach(({ note, duration }) => {
|
||||
try {
|
||||
const sound = pianoSounds.current![note]!;
|
||||
sound.playAsync().catch(console.error);
|
||||
setTimeout(() => {
|
||||
sound.stopAsync();
|
||||
}, duration - 10);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
currentCurIdx.current = idx;
|
||||
if (pianoSounds.current) {
|
||||
cursor.notes.forEach(({ note, duration }) => {
|
||||
try {
|
||||
const sound = pianoSounds.current![note]!;
|
||||
sound.playAsync().catch(console.error);
|
||||
setTimeout(() => {
|
||||
sound.stopAsync();
|
||||
}, duration - 10);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
partitionOffset.value = withTiming(
|
||||
-(cursor.x - data!.cursors[0]!.x) / partitionDims[0],
|
||||
{
|
||||
duration: transitionDuration,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
},
|
||||
() => {
|
||||
if (idx === data!.cursors.length - 1) {
|
||||
onEndReached();
|
||||
}
|
||||
}
|
||||
);
|
||||
setCurrentCurIdx(idx);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -130,6 +150,22 @@ const PartitionMagic = ({ songID, onEndReached, onError, onReady }: ParitionMagi
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{(!isPartitionSvgLoaded || isLoading) && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 50,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
}}
|
||||
>
|
||||
<LoadingComponent />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -148,15 +184,15 @@ const PartitionMagic = ({ songID, onEndReached, onError, onReady }: ParitionMagi
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{!isLoading && !isError && (
|
||||
<SvgContainer
|
||||
url={getSVGURL(songID)}
|
||||
onReady={onReady}
|
||||
style={{
|
||||
aspectRatio: partitionDims[0] / partitionDims[1],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SvgContainer
|
||||
url={getSVGURL(songID)}
|
||||
onReady={() => {
|
||||
setIsPartitionSvgLoaded(true);
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: partitionDims[0] / partitionDims[1],
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={{
|
||||
|
||||
@@ -242,6 +242,7 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
|
||||
<Animated.View style={[style, isDisabled ? disableStyle : animatedStyle]}>
|
||||
<Pressable
|
||||
focusable={focusable}
|
||||
tabIndex={focusable ? 0 : -1}
|
||||
disabled={isDisabled}
|
||||
onHoverIn={handleMouseEnter}
|
||||
onPressIn={handlePressIn}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Text, Row, Heading, Column } from 'native-base';
|
||||
import { Text, Heading } from 'native-base';
|
||||
import ButtonBase from './ButtonBase';
|
||||
import { View } from 'react-native';
|
||||
import { CloseSquare } from 'iconsax-react-native';
|
||||
import { ReactNode } from 'react';
|
||||
import Modal from 'react-native-modal';
|
||||
@@ -27,30 +28,45 @@ const PopupCC = ({ title, description, children, isVisible, setIsVisible }: Popu
|
||||
}}
|
||||
>
|
||||
<GlassmorphismCC>
|
||||
<Column
|
||||
<View
|
||||
style={{
|
||||
maxWidth: 800,
|
||||
padding: 20,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 10,
|
||||
position: 'relative',
|
||||
}}
|
||||
space={4}
|
||||
>
|
||||
{(setIsVisible || title) && (
|
||||
<Heading size="md" mb={2} alignItems={'flex-end'}>
|
||||
<Row style={{ flex: 1, width: '100%', alignItems: 'flex-end' }}>
|
||||
<Text style={{ flex: 1, width: '100%' }}>{title}</Text>
|
||||
{setIsVisible !== undefined && (
|
||||
<ButtonBase
|
||||
type="menu"
|
||||
icon={CloseSquare}
|
||||
onPress={async () => setIsVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</Heading>
|
||||
{setIsVisible !== undefined && (
|
||||
<ButtonBase
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
zIndex: 100,
|
||||
}}
|
||||
type="menu"
|
||||
icon={CloseSquare}
|
||||
onPress={async () => setIsVisible(false)}
|
||||
/>
|
||||
)}
|
||||
{title !== undefined && <Heading
|
||||
size="md"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Text style={{ flex: 1 }}>{title}</Text>
|
||||
</Heading> }
|
||||
{description !== undefined && <Text>{description}</Text>}
|
||||
{children !== undefined && children}
|
||||
</Column>
|
||||
</View>
|
||||
</GlassmorphismCC>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"pretty:check": "prettier --check .",
|
||||
"pretty:write": "prettier --write .",
|
||||
@@ -18,21 +18,22 @@
|
||||
"chromatic": "chromatic --exit-zero-on-changes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arthi-chaud/react-native-midi": "^0.0.6",
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"@motiz88/react-native-midi": "^0.0.6",
|
||||
"@react-native-async-storage/async-storage": "1.18.2",
|
||||
"@react-navigation/native": "^6.1.8",
|
||||
"@react-navigation/native-stack": "^6.9.14",
|
||||
"@reduxjs/toolkit": "^1.9.6",
|
||||
"expo": "~49.0.13",
|
||||
"expo-blur": "~12.4.1",
|
||||
"expo-av": "~13.4.1",
|
||||
"expo-blur": "~12.4.1",
|
||||
"expo-image-picker": "~14.3.2",
|
||||
"expo-linear-gradient": "~12.3.0",
|
||||
"expo-linking": "~5.0.2",
|
||||
"expo-screen-orientation": "~6.0.5",
|
||||
"expo-splash-screen": "~0.20.5",
|
||||
"expo-status-bar": "~1.6.0",
|
||||
"expo-system-ui": "~2.4.0",
|
||||
"i18next": "^23.5.1",
|
||||
"iconsax-react-native": "^0.0.8",
|
||||
"native-base": "^3.4.28",
|
||||
|
||||
32
front/rnw.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import 'react-native';
|
||||
|
||||
declare module 'react-native' {
|
||||
interface PressableStateCallbackType {
|
||||
hovered?: boolean;
|
||||
focused?: boolean;
|
||||
}
|
||||
interface AccessibilityProps {
|
||||
tabIndex?: number;
|
||||
}
|
||||
interface ViewStyle {
|
||||
transitionProperty?: string;
|
||||
transitionDuration?: string;
|
||||
}
|
||||
interface TextProps {
|
||||
href?: string;
|
||||
hrefAttrs?: {
|
||||
rel?: 'noreferrer';
|
||||
target?: string;
|
||||
};
|
||||
}
|
||||
interface ViewProps {
|
||||
dataSet?: Record<string, string>;
|
||||
href?: string;
|
||||
hrefAttrs?: {
|
||||
rel: 'noreferrer';
|
||||
target?: '_blank';
|
||||
};
|
||||
onClick?: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@
|
||||
"app.config.ts",
|
||||
"*/*.test.tsx",
|
||||
"web-build",
|
||||
"dist"
|
||||
"dist",
|
||||
"android"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
import { StackActions } from '@react-navigation/native';
|
||||
import React, { useEffect, useRef, useState, createContext } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { SafeAreaView, Platform } from 'react-native';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
@@ -15,16 +15,15 @@ import { Text, Row, View, useToast } from 'native-base';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import { useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
import LoadingComponent, { LoadingView } from '../components/Loading';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../state/Store';
|
||||
import { Translate, translate } from '../i18n/i18n';
|
||||
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
|
||||
import { useStopwatch } from 'react-use-precision-timer';
|
||||
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@arthi-chaud/react-native-midi';
|
||||
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
|
||||
import * as Linking from 'expo-linking';
|
||||
import url from 'url';
|
||||
import { PianoCanvasContext } from '../models/PianoGame';
|
||||
import PartitionMagic from '../components/Play/PartitionMagic';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
@@ -69,15 +68,8 @@ 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, route }: RouteProps<PlayViewProps>) => {
|
||||
const [type, setType] = useState<'practice' | 'normal'>();
|
||||
const [playType, setPlayType] = useState<'practice' | 'normal' | null>(null);
|
||||
const accessToken = useSelector((state: RootState) => state.user.accessToken);
|
||||
const navigation = useNavigation();
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
@@ -91,13 +83,11 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
const [time, setTime] = useState(0);
|
||||
const [endResult, setEndResult] = useState<unknown>();
|
||||
const songHistory = useQuery(API.getSongHistory(songId));
|
||||
const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render
|
||||
const [score, setScore] = useState(0); // Between 0 and 100
|
||||
// const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
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]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [streak, setStreak] = useState(0);
|
||||
const scoreMessageScale = useSharedValue(0);
|
||||
@@ -113,6 +103,7 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
const statColor = colors.lightText;
|
||||
|
||||
const onPause = () => {
|
||||
console.log('onPause');
|
||||
stopwatch.pause();
|
||||
setPause(true);
|
||||
webSocket.current?.send(
|
||||
@@ -167,7 +158,7 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
JSON.stringify({
|
||||
type: 'start',
|
||||
id: song.data!.id,
|
||||
mode: type,
|
||||
mode: playType,
|
||||
bearer: accessToken,
|
||||
})
|
||||
);
|
||||
@@ -185,6 +176,11 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
webSocket.current.onmessage = (message) => {
|
||||
try {
|
||||
const data = JSON.parse(message.data);
|
||||
if (data.error) {
|
||||
console.error('Scoro msg: ', data.error);
|
||||
toast.show({ description: 'Scoro: ' + data.error });
|
||||
return;
|
||||
}
|
||||
if (data.type == 'end') {
|
||||
endMsgReceived = true;
|
||||
webSocket.current?.close();
|
||||
@@ -234,19 +230,9 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
};
|
||||
inputs.forEach((input) => {
|
||||
input.onmidimessage = (message) => {
|
||||
console.log('onmessage');
|
||||
const { command, note } = parseMidiMessage(message);
|
||||
const keyIsPressed = command == 9;
|
||||
if (keyIsPressed) {
|
||||
setPressedKeys((prev) => {
|
||||
prev.set(note, getElapsedTime());
|
||||
return prev;
|
||||
});
|
||||
} else {
|
||||
setPressedKeys((prev) => {
|
||||
prev.delete(note);
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
webSocket.current?.send(
|
||||
JSON.stringify({
|
||||
@@ -267,7 +253,7 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
||||
const interval = setInterval(() => {
|
||||
setTime(() => getElapsedTime()); // Countdown
|
||||
}, 1);
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
ScreenOrientation.unlockAsync().catch(() => {});
|
||||
@@ -298,10 +284,10 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
if (navigation.getState().routes.at(-1)?.name != route.name) {
|
||||
return;
|
||||
}
|
||||
if (song.data && !webSocket.current && partitionRendered) {
|
||||
if (playType && song.data && !webSocket.current) {
|
||||
requestMIDIAccess().then(onMIDISuccess).catch(onMIDIFailure);
|
||||
}
|
||||
}, [song.data, partitionRendered]);
|
||||
}, [song.data, playType]);
|
||||
|
||||
if (!song.data) {
|
||||
return <LoadingView />;
|
||||
@@ -342,7 +328,7 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
<PopupCC
|
||||
title={translate('selectPlayMode')}
|
||||
description={translate('selectPlayModeExplaination')}
|
||||
isVisible={type === undefined}
|
||||
isVisible={playType == null}
|
||||
setIsVisible={
|
||||
navigation.canGoBack()
|
||||
? (isVisible) => {
|
||||
@@ -359,13 +345,13 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
style={{}}
|
||||
type="outlined"
|
||||
title={translate('practiceBtn')}
|
||||
onPress={async () => setType('practice')}
|
||||
onPress={async () => setPlayType('practice')}
|
||||
/>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
type="filled"
|
||||
title={translate('playBtn')}
|
||||
onPress={async () => setType('normal')}
|
||||
onPress={async () => setPlayType('normal')}
|
||||
/>
|
||||
</Row>
|
||||
</PopupCC>
|
||||
@@ -468,23 +454,18 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
>
|
||||
<PianoCC.Provider
|
||||
value={{
|
||||
pressedKeys: pressedKeys,
|
||||
timestamp: time,
|
||||
messages: [],
|
||||
<PartitionMagic
|
||||
timestamp={time}
|
||||
songID={song.data.id}
|
||||
onEndReached={() => {
|
||||
setTimeout(() => {
|
||||
onEnd();
|
||||
}, 500);
|
||||
}}
|
||||
>
|
||||
<PartitionMagic
|
||||
songID={song.data.id}
|
||||
onReady={() => setPartitionRendered(true)}
|
||||
onEndReached={onEnd}
|
||||
onError={() => {
|
||||
console.log('error from partition magic');
|
||||
}}
|
||||
/>
|
||||
</PianoCC.Provider>
|
||||
{!partitionRendered && <LoadingComponent />}
|
||||
onError={() => {
|
||||
console.log('error from partition magic');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<PlayViewControlBar
|
||||
score={score}
|
||||
@@ -496,6 +477,36 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
/>
|
||||
<PopupCC
|
||||
title={translate('selectPlayMode')}
|
||||
description={translate('selectPlayModeExplaination')}
|
||||
isVisible={!playType}
|
||||
setIsVisible={
|
||||
navigation.canGoBack()
|
||||
? (isVisible) => {
|
||||
if (!isVisible) {
|
||||
// If we dismiss the popup, Go to previous page
|
||||
navigation.goBack();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Row style={{ justifyContent: 'space-between' }}>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
type="outlined"
|
||||
title={translate('practiceBtn')}
|
||||
onPress={async () => setPlayType('practice')}
|
||||
/>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
type="filled"
|
||||
title={translate('playBtn')}
|
||||
onPress={async () => setPlayType('normal')}
|
||||
/>
|
||||
</Row>
|
||||
</PopupCC>
|
||||
</SafeAreaView>
|
||||
{colorScheme === 'dark' && (
|
||||
<LinearGradient
|
||||
|
||||
@@ -74,12 +74,12 @@ const SigninView = () => {
|
||||
value={formData.username.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.username
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
});
|
||||
try {
|
||||
validationSchemas.username.validateSync(t);
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
}}
|
||||
isRequired
|
||||
/>,
|
||||
@@ -93,12 +93,12 @@ const SigninView = () => {
|
||||
value={formData.password.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.password
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, password: { value: t, error } });
|
||||
});
|
||||
try {
|
||||
validationSchemas.password.validateSync(t);
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
setFormData({ ...formData, password: { value: t, error } });
|
||||
}}
|
||||
isRequired
|
||||
isSecret
|
||||
|
||||
@@ -88,12 +88,12 @@ const SignupView = () => {
|
||||
value={formData.username.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.username
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
});
|
||||
try {
|
||||
validationSchemas.username.validateSync(t);
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
}}
|
||||
isRequired
|
||||
/>,
|
||||
@@ -106,12 +106,12 @@ const SignupView = () => {
|
||||
value={formData.email.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.email
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, email: { value: t, error } });
|
||||
});
|
||||
try {
|
||||
validationSchemas.email.validateSync(t);
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
setFormData({ ...formData, email: { value: t, error } });
|
||||
}}
|
||||
isRequired
|
||||
/>,
|
||||
@@ -126,12 +126,12 @@ const SignupView = () => {
|
||||
value={formData.password.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.password
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, password: { value: t, error } });
|
||||
});
|
||||
try {
|
||||
validationSchemas.password.validateSync(t);
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
setFormData({ ...formData, password: { value: t, error } });
|
||||
}}
|
||||
/>,
|
||||
<TextFormField
|
||||
@@ -145,18 +145,18 @@ const SignupView = () => {
|
||||
value={formData.repeatPassword.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.password
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
if (!error && t !== formData.password.value) {
|
||||
error = translate('passwordsDontMatch');
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
repeatPassword: { value: t, error },
|
||||
});
|
||||
});
|
||||
try {
|
||||
validationSchemas.password.validateSync(t);
|
||||
} catch (e: any) {
|
||||
error = e.message;
|
||||
}
|
||||
if (!error && t !== formData.password.value) {
|
||||
error = translate('passwordsDontMatch');
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
repeatPassword: { value: t, error },
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
|
||||
@@ -20,13 +20,6 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@arthi-chaud/react-native-midi@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@arthi-chaud/react-native-midi/-/react-native-midi-0.0.6.tgz#7f17cc72799aff2c040198191dfec7308d67dc9a"
|
||||
integrity sha512-U4+DKie+AINcWEPlKXglaB7AEOZSfjA1p6xy/M9JGZBQpgYGEwMkA6WEbfaycGeB3+17Be8jR6LclLweSBdayw==
|
||||
dependencies:
|
||||
event-target-shim "^6.0.2"
|
||||
|
||||
"@babel/code-frame@7.10.4", "@babel/code-frame@~7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
|
||||
@@ -1991,6 +1984,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
||||
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
||||
|
||||
"@motiz88/react-native-midi@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@motiz88/react-native-midi/-/react-native-midi-0.0.6.tgz#d212e9924dfa1168da82748cabe82a04437c6907"
|
||||
integrity sha512-0cRYgTjWqjv74Gl94ruX8WBiGQBconU5j/Hy0P7NlsVTRmgnqME3cvW38JMdXb9MxYuA+7BLq0VKkE2DXswpEw==
|
||||
dependencies:
|
||||
event-target-shim "^6.0.2"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -5918,6 +5918,14 @@ expo-status-bar@~1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.6.0.tgz#e79ffdb9a84d2e0ec9a0dc7392d9ab364fefa9cf"
|
||||
integrity sha512-e//Oi2WPdomMlMDD3skE4+1ZarKCJ/suvcB4Jo/nO427niKug5oppcPNYO+csR6y3ZglGuypS+3pp/hJ+Xp6fQ==
|
||||
|
||||
expo-system-ui@~2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-2.4.0.tgz#bf809172726cf661ab4f526129eb427bb5f6e4d4"
|
||||
integrity sha512-uaBAYeQtFzyE8WVcch2V3G243xvOf7vJkLzrIJ3rJ8NA3uZRmxF0lMMe75oMrAaLVXyr1Z+ZE6UZwh7x49FuIg==
|
||||
dependencies:
|
||||
"@react-native/normalize-color" "^2.0.0"
|
||||
debug "^4.3.2"
|
||||
|
||||
expo-updates-interface@~0.10.0:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-0.10.1.tgz#cab075641cd381718ccd9264bf133dc393430a44"
|
||||
|
||||