Compare commits
5
Commits
v0.9.2
...
clem-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae0bbea3a | ||
|
|
f5136ae59b | ||
|
|
f632ed42a3 | ||
|
|
00ee5cd531 | ||
|
|
1d496301d9 |
@@ -8,20 +8,17 @@ import { Audio } from 'expo-av';
|
|||||||
import { SvgContainer } from './SvgContainer';
|
import { SvgContainer } from './SvgContainer';
|
||||||
import LoadingComponent from '../Loading';
|
import LoadingComponent from '../Loading';
|
||||||
import { SplendidGrandPiano } from 'smplr';
|
import { SplendidGrandPiano } from 'smplr';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import { useStopwatch } from 'react-use-precision-timer';
|
||||||
|
|
||||||
|
export const timestampAtom = atom(0);
|
||||||
|
export const shouldPlayAtom = atom(false);
|
||||||
|
export const partitionStateAtom = atom(
|
||||||
|
'loading' as 'loading' | 'ready' | 'playing' | 'paused' | 'ended' | 'error'
|
||||||
|
);
|
||||||
|
|
||||||
export type ParitionMagicProps = {
|
export type ParitionMagicProps = {
|
||||||
timestamp: number;
|
|
||||||
songID: number;
|
songID: number;
|
||||||
shouldPlay: boolean;
|
|
||||||
onEndReached: () => void;
|
|
||||||
onError: (err: string) => void;
|
|
||||||
onReady: () => void;
|
|
||||||
onPlay: () => void;
|
|
||||||
onPause: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSVGURL = (songID: number) => {
|
|
||||||
return API.getPartitionSvgUrl(songID);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCursorToPlay = (
|
const getCursorToPlay = (
|
||||||
@@ -37,30 +34,38 @@ const getCursorToPlay = (
|
|||||||
const cursorInfo = cursorInfos[i]!;
|
const cursorInfo = cursorInfos[i]!;
|
||||||
if (cursorInfo.timestamp <= timestamp) {
|
if (cursorInfo.timestamp <= timestamp) {
|
||||||
onCursorMove(cursorInfo, i);
|
onCursorMove(cursorInfo, i);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const transitionDuration = 50;
|
const startResumePauseWatch = (stopwatch: ReturnType<typeof useStopwatch>, shouldPlay: boolean) => {
|
||||||
|
if (shouldPlay) {
|
||||||
|
if (stopwatch.isPaused()) {
|
||||||
|
stopwatch.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopwatch.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopwatch.pause();
|
||||||
|
};
|
||||||
|
|
||||||
const PartitionMagic = ({
|
const transitionDuration = 200;
|
||||||
timestamp,
|
|
||||||
songID,
|
const PartitionMagic = ({ songID }: ParitionMagicProps) => {
|
||||||
shouldPlay,
|
|
||||||
onEndReached,
|
|
||||||
onError,
|
|
||||||
onReady,
|
|
||||||
onPlay,
|
|
||||||
onPause,
|
|
||||||
}: ParitionMagicProps) => {
|
|
||||||
const { data, isLoading, isError } = useQuery(API.getSongCursorInfos(songID));
|
const { data, isLoading, isError } = useQuery(API.getSongCursorInfos(songID));
|
||||||
const currentCurIdx = React.useRef(-1);
|
const currentCurIdx = React.useRef(-1);
|
||||||
|
const stopwatch = useStopwatch();
|
||||||
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
||||||
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
|
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
|
||||||
const partitionOffset = useSharedValue(0);
|
const partitionOffset = useSharedValue(0);
|
||||||
const melodySound = React.useRef<Audio.Sound | null>(null);
|
const melodySound = React.useRef<Audio.Sound | null>(null);
|
||||||
const piano = React.useRef<SplendidGrandPiano | null>(null);
|
const piano = React.useRef<SplendidGrandPiano | null>(null);
|
||||||
const [isPianoLoaded, setIsPianoLoaded] = React.useState(false);
|
const [isPianoLoaded, setIsPianoLoaded] = React.useState(false);
|
||||||
|
const [timestamp, setTimestamp] = useAtom(timestampAtom);
|
||||||
|
const [shouldPlay, setShouldPlay] = useAtom(shouldPlayAtom);
|
||||||
|
const [partitionState, setPartitionState] = useAtom(partitionStateAtom);
|
||||||
const cursorPaddingVertical = 10;
|
const cursorPaddingVertical = 10;
|
||||||
const cursorPaddingHorizontal = 3;
|
const cursorPaddingHorizontal = 3;
|
||||||
|
|
||||||
@@ -72,12 +77,36 @@ const PartitionMagic = ({
|
|||||||
const cursorTop = (data?.cursors[cursorDisplayIdx]?.y ?? 0) - cursorPaddingVertical;
|
const cursorTop = (data?.cursors[cursorDisplayIdx]?.y ?? 0) - cursorPaddingVertical;
|
||||||
const cursorLeft = (data?.cursors[0]?.x ?? 0) - cursorPaddingHorizontal;
|
const cursorLeft = (data?.cursors[0]?.x ?? 0) - cursorPaddingHorizontal;
|
||||||
|
|
||||||
|
console.log('state', partitionState, timestamp);
|
||||||
|
|
||||||
if (!endPartitionReached && currentCurIdx.current + 1 === data?.cursors.length) {
|
if (!endPartitionReached && currentCurIdx.current + 1 === data?.cursors.length) {
|
||||||
// weird contraption but the mobile don't want classic functions to be called
|
// weird contraption but the mobile don't want classic functions to be called
|
||||||
// with the withTiming function :(
|
// with the withTiming function :(
|
||||||
setEndPartitionReached(true);
|
melodySound.current?.pauseAsync();
|
||||||
|
piano.current?.stop();
|
||||||
|
setPartitionState('ended');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
|
setPartitionState('error');
|
||||||
|
}
|
||||||
|
}, [isError]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isPartitionSvgLoaded && !isLoading && (melodySound.current?._loaded || isPianoLoaded)) {
|
||||||
|
setPartitionState('ready');
|
||||||
|
} else if (partitionState !== 'loading') {
|
||||||
|
setPartitionState('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setPartitionState('loading');
|
||||||
|
setTimestamp(0);
|
||||||
|
setShouldPlay(false);
|
||||||
|
};
|
||||||
|
}, [isPartitionSvgLoaded, isLoading, melodySound.current?._loaded, isPianoLoaded]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (Platform.OS === 'web' && !piano.current) {
|
if (Platform.OS === 'web' && !piano.current) {
|
||||||
const audio = new AudioContext();
|
const audio = new AudioContext();
|
||||||
@@ -86,14 +115,9 @@ const PartitionMagic = ({
|
|||||||
setIsPianoLoaded(true);
|
setIsPianoLoaded(true);
|
||||||
});
|
});
|
||||||
} else if (!melodySound.current) {
|
} else if (!melodySound.current) {
|
||||||
Audio.Sound.createAsync(
|
Audio.Sound.createAsync({
|
||||||
{
|
uri: API.getPartitionMelodyUrl(songID),
|
||||||
uri: API.getPartitionMelodyUrl(songID),
|
}).then((track) => {
|
||||||
},
|
|
||||||
{
|
|
||||||
progressUpdateIntervalMillis: 200,
|
|
||||||
}
|
|
||||||
).then((track) => {
|
|
||||||
melodySound.current = track.sound;
|
melodySound.current = track.sound;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -115,72 +139,38 @@ const PartitionMagic = ({
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (onError && isError) {
|
const interval = setInterval(
|
||||||
onError('Error while loading partition');
|
() => {
|
||||||
return;
|
// if (partitionState !== 'playing') return;
|
||||||
}
|
setTimestamp(stopwatch.getElapsedRunningTime());
|
||||||
}, [onError, isError]);
|
},
|
||||||
|
Platform.OS === 'web' ? 200 : 500
|
||||||
React.useEffect(() => {
|
);
|
||||||
if (isPartitionSvgLoaded && !isLoading && (melodySound.current?._loaded || isPianoLoaded)) {
|
return () => {
|
||||||
onReady();
|
clearInterval(interval);
|
||||||
}
|
};
|
||||||
}, [isPartitionSvgLoaded, isLoading, melodySound.current?._loaded, isPianoLoaded]);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
if (!piano.current || !isPianoLoaded) {
|
if (!piano.current || !isPianoLoaded) return;
|
||||||
return;
|
setPartitionState(shouldPlay ? 'playing' : 'paused');
|
||||||
}
|
startResumePauseWatch(stopwatch, shouldPlay);
|
||||||
shouldPlay ? onPlay() : onPause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!melodySound.current || !melodySound.current._loaded) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!melodySound.current || !melodySound.current._loaded) return;
|
||||||
|
setPartitionState(shouldPlay ? 'playing' : 'paused');
|
||||||
|
startResumePauseWatch(stopwatch, shouldPlay);
|
||||||
if (shouldPlay) {
|
if (shouldPlay) {
|
||||||
melodySound.current.playAsync().then(onPlay).catch(console.error);
|
melodySound.current.playAsync().catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
melodySound.current.pauseAsync().then(onPause).catch(console.error);
|
melodySound.current.pauseAsync().catch(console.error);
|
||||||
}
|
}
|
||||||
}, [shouldPlay]);
|
}, [shouldPlay]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (endPartitionReached) {
|
|
||||||
// if the audio is unsync
|
|
||||||
melodySound.current?.pauseAsync();
|
|
||||||
onEndReached();
|
|
||||||
}
|
|
||||||
}, [endPartitionReached]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!melodySound.current || !melodySound.current._loaded) return;
|
|
||||||
if (!data || data?.cursors.length === 0) return;
|
|
||||||
|
|
||||||
melodySound.current.setOnPlaybackStatusUpdate((status) => {
|
|
||||||
//@ts-expect-error positionMillis is not in the type
|
|
||||||
const timestamp = status?.positionMillis ?? 0;
|
|
||||||
getCursorToPlay(
|
|
||||||
data!.cursors,
|
|
||||||
currentCurIdx.current,
|
|
||||||
timestamp + transitionDuration,
|
|
||||||
(cursor, idx) => {
|
|
||||||
currentCurIdx.current = idx;
|
|
||||||
partitionOffset.value = withTiming(
|
|
||||||
-(cursor.x - data!.cursors[0]!.x) / partitionDims[0],
|
|
||||||
{
|
|
||||||
duration: transitionDuration,
|
|
||||||
easing: Easing.inOut(Easing.ease),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [data?.cursors, melodySound.current?._loaded]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!shouldPlay) return;
|
if (!shouldPlay) return;
|
||||||
if (!piano.current || !isPianoLoaded) return;
|
if (!melodySound.current || !melodySound.current._loaded) return;
|
||||||
if (!data || data?.cursors.length === 0) return;
|
if (!data || data?.cursors.length === 0) return;
|
||||||
getCursorToPlay(
|
getCursorToPlay(
|
||||||
data!.cursors,
|
data!.cursors,
|
||||||
@@ -195,6 +185,7 @@ const PartitionMagic = ({
|
|||||||
easing: Easing.inOut(Easing.ease),
|
easing: Easing.inOut(Easing.ease),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if (!piano.current || !isPianoLoaded) return;
|
||||||
cursor.notes.forEach((note) => {
|
cursor.notes.forEach((note) => {
|
||||||
piano.current?.start({
|
piano.current?.start({
|
||||||
note: note.note,
|
note: note.note,
|
||||||
@@ -249,7 +240,7 @@ const PartitionMagic = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SvgContainer
|
<SvgContainer
|
||||||
url={getSVGURL(songID)}
|
url={API.getPartitionSvgUrl(songID)}
|
||||||
onReady={() => {
|
onReady={() => {
|
||||||
setIsPartitionSvgLoaded(true);
|
setIsPartitionSvgLoaded(true);
|
||||||
}}
|
}}
|
||||||
@@ -277,11 +268,4 @@ const PartitionMagic = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
PartitionMagic.defaultProps = {
|
|
||||||
onError: () => {},
|
|
||||||
onReady: () => {},
|
|
||||||
onPlay: () => {},
|
|
||||||
onPause: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PartitionMagic;
|
export default PartitionMagic;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Text, useTheme } from 'native-base';
|
||||||
|
import { timestampAtom, partitionStateAtom } from './PartitionMagic';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
export const TimestampDisplay = () => {
|
||||||
|
const [time] = useAtom(timestampAtom);
|
||||||
|
const [partitionState] = useAtom(partitionStateAtom);
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const textColor = colors.text;
|
||||||
|
const paused = partitionState === 'paused';
|
||||||
|
if (time < 0) {
|
||||||
|
if (paused) {
|
||||||
|
return <Text color={textColor[900]}>0:00</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text color={textColor[900]}>
|
||||||
|
{Math.floor((time % 60000) / 1000)
|
||||||
|
.toFixed(0)
|
||||||
|
.toString()}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text color={textColor[900]}>
|
||||||
|
{`${Math.floor(time / 60000)}:${Math.floor((time % 60000) / 1000)
|
||||||
|
.toFixed(0)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import PopupCC from '../UI/PopupCC';
|
||||||
|
import { shouldEndAtom } from './PlayViewControlBar';
|
||||||
|
import { partitionStateAtom } from './PartitionMagic';
|
||||||
|
import ScoreModal from '../ScoreModal';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
export const PlayEndModal = () => {
|
||||||
|
const [shouldEnd] = useAtom(shouldEndAtom);
|
||||||
|
const [partitionState] = useAtom(partitionStateAtom);
|
||||||
|
|
||||||
|
const isEnd = shouldEnd || partitionState === 'ended';
|
||||||
|
return (
|
||||||
|
<PopupCC isVisible={isEnd}>
|
||||||
|
<ScoreModal
|
||||||
|
songId={0}
|
||||||
|
overallScore={0}
|
||||||
|
precision={0}
|
||||||
|
score={{
|
||||||
|
max_score: 0,
|
||||||
|
missed: 0,
|
||||||
|
wrong: 0,
|
||||||
|
good: 0,
|
||||||
|
great: 0,
|
||||||
|
perfect: 0,
|
||||||
|
current_streak: 0,
|
||||||
|
max_streak: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopupCC>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,20 +10,21 @@ import Animated, {
|
|||||||
Easing,
|
Easing,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
|
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
|
|
||||||
|
export const scoreMessageAtom = atom<ScoreMessage | null>(null);
|
||||||
|
export const scoreAtom = atom(0);
|
||||||
|
|
||||||
export type ScoreMessage = {
|
export type ScoreMessage = {
|
||||||
content: string;
|
content: string;
|
||||||
color?: ColorSchemeType;
|
color?: ColorSchemeType;
|
||||||
id: number;
|
timestamp: number;
|
||||||
};
|
|
||||||
|
|
||||||
type PlayScoreProps = {
|
|
||||||
score: number;
|
|
||||||
streak: number;
|
streak: number;
|
||||||
message?: ScoreMessage;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PlayScore = ({ score, streak, message }: PlayScoreProps) => {
|
export const PlayScore = () => {
|
||||||
|
const [message] = useAtom(scoreMessageAtom);
|
||||||
|
const [score] = useAtom(scoreAtom);
|
||||||
const scoreMessageScale = useSharedValue(0);
|
const scoreMessageScale = useSharedValue(0);
|
||||||
// this style should bounce in on enter and fade away
|
// this style should bounce in on enter and fade away
|
||||||
const scoreMsgStyle = useAnimatedStyle(() => {
|
const scoreMsgStyle = useAnimatedStyle(() => {
|
||||||
@@ -92,9 +93,9 @@ export const PlayScore = ({ score, streak, message }: PlayScoreProps) => {
|
|||||||
<Text color={textColor[900]} fontSize={20}>
|
<Text color={textColor[900]} fontSize={20}>
|
||||||
{message.content}
|
{message.content}
|
||||||
</Text>
|
</Text>
|
||||||
{streak > 0 && (
|
{message.streak > 0 && (
|
||||||
<Text color={textColor[900]} fontSize={15} bold>
|
<Text color={textColor[900]} fontSize={15} bold>
|
||||||
{`x${streak}`}
|
{`x${message.streak}`}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Row, Image, Text, useBreakpointValue, IconButton } from 'native-base';
|
import { Row, Image, Text, useBreakpointValue, IconButton, Button } from 'native-base';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { MetronomeControls } from '../Metronome';
|
import { MetronomeControls } from '../Metronome';
|
||||||
import StarProgress from '../StarProgress';
|
import StarProgress from '../StarProgress';
|
||||||
import Song from '../../models/Song';
|
import Song from '../../models/Song';
|
||||||
import { useTheme } from 'native-base';
|
import { useTheme } from 'native-base';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import { partitionStateAtom, shouldPlayAtom } from './PartitionMagic';
|
||||||
|
import { TimestampDisplay } from './PartitionTimestampText';
|
||||||
|
|
||||||
|
export const shouldEndAtom = atom(false);
|
||||||
|
|
||||||
type PlayViewControlBarProps = {
|
type PlayViewControlBarProps = {
|
||||||
song: Song;
|
song: Song;
|
||||||
time: number;
|
|
||||||
paused: boolean;
|
|
||||||
score: number;
|
|
||||||
disabled: boolean;
|
|
||||||
onResume: () => void;
|
|
||||||
onPause: () => void;
|
|
||||||
onEnd: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlayViewControlBar = ({
|
const PlayViewControlBar = ({ song }: PlayViewControlBarProps) => {
|
||||||
song,
|
const [, setShouldPlay] = useAtom(shouldPlayAtom);
|
||||||
time,
|
const [partitionState] = useAtom(partitionStateAtom);
|
||||||
paused,
|
const [, setShouldEnd] = useAtom(shouldEndAtom);
|
||||||
score,
|
|
||||||
disabled,
|
|
||||||
onResume,
|
|
||||||
onPause,
|
|
||||||
onEnd,
|
|
||||||
}: PlayViewControlBarProps) => {
|
|
||||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
const isPhone = screenSize === 'small';
|
const isPhone = screenSize === 'small';
|
||||||
const bpm = React.useRef<number>(60);
|
const bpm = React.useRef<number>(60);
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const textColor = colors.text;
|
const textColor = colors.text;
|
||||||
|
const isPlaying = partitionState === 'playing';
|
||||||
|
const isPartitionLoading = partitionState === 'loading';
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setShouldPlay(false);
|
||||||
|
setShouldEnd(false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
style={{
|
style={{
|
||||||
@@ -107,17 +109,20 @@ const PlayViewControlBar = ({
|
|||||||
gap: isPhone ? 10 : 25,
|
gap: isPhone ? 10 : 25,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
{isPartitionLoading ? (
|
||||||
size="sm"
|
<Button isLoading size="sm" variant="solid" color={colors.coolGray[900]} />
|
||||||
variant="solid"
|
) : (
|
||||||
disabled={disabled}
|
<IconButton
|
||||||
_icon={{
|
size="sm"
|
||||||
as: Ionicons,
|
variant="solid"
|
||||||
color: colors.coolGray[900],
|
_icon={{
|
||||||
name: paused ? 'play' : 'pause',
|
as: Ionicons,
|
||||||
}}
|
color: colors.coolGray[900],
|
||||||
onPress={paused ? onResume : onPause}
|
name: isPlaying ? 'pause' : 'play',
|
||||||
/>
|
}}
|
||||||
|
onPress={() => setShouldPlay(!isPlaying)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
colorScheme="coolGray"
|
colorScheme="coolGray"
|
||||||
@@ -126,22 +131,10 @@ const PlayViewControlBar = ({
|
|||||||
as: Ionicons,
|
as: Ionicons,
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
}}
|
}}
|
||||||
onPress={onEnd}
|
onPress={() => setShouldEnd(true)}
|
||||||
/>
|
/>
|
||||||
<Text color={textColor[900]}>
|
<TimestampDisplay />
|
||||||
{time < 0
|
|
||||||
? paused
|
|
||||||
? '0:00'
|
|
||||||
: Math.floor((time % 60000) / 1000)
|
|
||||||
.toFixed(0)
|
|
||||||
.toString()
|
|
||||||
: `${Math.floor(time / 60000)}:${Math.floor((time % 60000) / 1000)
|
|
||||||
.toFixed(0)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}`}
|
|
||||||
</Text>
|
|
||||||
<StarProgress
|
<StarProgress
|
||||||
value={score}
|
|
||||||
max={100}
|
max={100}
|
||||||
starSteps={[50, 75, 90]}
|
starSteps={[50, 75, 90]}
|
||||||
style={{
|
style={{
|
||||||
@@ -162,17 +155,10 @@ const PlayViewControlBar = ({
|
|||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MetronomeControls paused={paused} bpm={bpm.current} />
|
<MetronomeControls paused={isPlaying} bpm={bpm.current} />
|
||||||
</View>
|
</View>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
PlayViewControlBar.defaultProps = {
|
|
||||||
onResume: () => {},
|
|
||||||
onPause: () => {},
|
|
||||||
onEnd: () => {},
|
|
||||||
disabled: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PlayViewControlBar;
|
export default PlayViewControlBar;
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import * as React from 'react';
|
|||||||
import { Progress } from 'native-base';
|
import { Progress } from 'native-base';
|
||||||
import { View, ViewStyle, StyleProp } from 'react-native';
|
import { View, ViewStyle, StyleProp } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { scoreAtom } from './Play/PlayScore';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
export interface StarProgressProps {
|
export interface StarProgressProps {
|
||||||
value: number;
|
|
||||||
max: number;
|
max: number;
|
||||||
starSteps: number[];
|
starSteps: number[];
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StarProgress = (props: StarProgressProps) => {
|
const StarProgress = (props: StarProgressProps) => {
|
||||||
|
const [score] = useAtom(scoreAtom);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -29,15 +31,15 @@ const StarProgress = (props: StarProgressProps) => {
|
|||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
value={props.value}
|
value={score}
|
||||||
max={props.max}
|
max={props.max}
|
||||||
/>
|
/>
|
||||||
{props.starSteps.map((step) => {
|
{props.starSteps.map((step) => {
|
||||||
return (
|
return (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
key={step}
|
key={step}
|
||||||
name={step <= props.value ? 'star' : 'star-outline'}
|
name={step <= score ? 'star' : 'star-outline'}
|
||||||
color={step <= props.value ? '#EBDA3C' : '#6075F9'}
|
color={step <= score ? '#EBDA3C' : '#6075F9'}
|
||||||
size={20}
|
size={20}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"fbjs": "^3.0.5",
|
"fbjs": "^3.0.5",
|
||||||
"i18next": "^23.5.1",
|
"i18next": "^23.5.1",
|
||||||
"iconsax-react-native": "^0.0.8",
|
"iconsax-react-native": "^0.0.8",
|
||||||
|
"jotai": "^2.6.1",
|
||||||
"native-base": "^3.4.28",
|
"native-base": "^3.4.28",
|
||||||
"normalize-css-color": "^1.0.2",
|
"normalize-css-color": "^1.0.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|||||||
+46
-46
@@ -25,6 +25,7 @@ import { Clock, Cup } from 'iconsax-react-native';
|
|||||||
import PlayViewControlBar from '../components/Play/PlayViewControlBar';
|
import PlayViewControlBar from '../components/Play/PlayViewControlBar';
|
||||||
import ScoreModal from '../components/ScoreModal';
|
import ScoreModal from '../components/ScoreModal';
|
||||||
import { PlayScore, ScoreMessage } from '../components/Play/PlayScore';
|
import { PlayScore, ScoreMessage } from '../components/Play/PlayScore';
|
||||||
|
import { PlayEndModal } from '../components/Play/PlayEndModal';
|
||||||
|
|
||||||
type PlayViewProps = {
|
type PlayViewProps = {
|
||||||
songId: number;
|
songId: number;
|
||||||
@@ -63,22 +64,22 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
|||||||
const isPhone = screenSize === 'small';
|
const isPhone = screenSize === 'small';
|
||||||
const song = useQuery(API.getSong(songId, ['artist']), { staleTime: Infinity });
|
const song = useQuery(API.getSong(songId, ['artist']), { staleTime: Infinity });
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
// const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
||||||
const webSocket = useRef<WebSocket>();
|
const webSocket = useRef<WebSocket>();
|
||||||
const [paused, setPause] = useState<boolean>(true);
|
// const [paused, setPause] = useState<boolean>(true);
|
||||||
const stopwatch = useStopwatch();
|
// const stopwatch = useStopwatch();
|
||||||
const [time, setTime] = useState(0);
|
// const [time, setTime] = useState(0);
|
||||||
const [endResult, setEndResult] = useState<unknown>();
|
const [endResult, setEndResult] = useState<unknown>();
|
||||||
const [shouldPlay, setShouldPlay] = useState(false);
|
// const [shouldPlay, setShouldPlay] = useState(false);
|
||||||
const songHistory = useQuery(API.getSongHistory(songId));
|
const songHistory = useQuery(API.getSongHistory(songId));
|
||||||
const [score, setScore] = useState(0); // Between 0 and 100
|
// const [score, setScore] = useState(0); // Between 0 and 100
|
||||||
const getElapsedTime = () => stopwatch.getElapsedRunningTime();
|
// const getElapsedTime = () => stopwatch.getElapsedRunningTime();
|
||||||
const [readyToPlay, setReadyToPlay] = useState(false);
|
// const [readyToPlay, setReadyToPlay] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [midiKeyboardFound, setMidiKeyboardFound] = useState<boolean>();
|
const [midiKeyboardFound, setMidiKeyboardFound] = useState<boolean>();
|
||||||
// first number is the note, the other is the time when pressed on release the key is removed
|
// first number is the note, the other is the time when pressed on release the key is removed
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [streak, setStreak] = useState(0);
|
// const [streak, setStreak] = useState(0);
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const statColor = colors.lightText;
|
const statColor = colors.lightText;
|
||||||
@@ -131,7 +132,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
|||||||
console.log('MIDI inputs', inputs);
|
console.log('MIDI inputs', inputs);
|
||||||
let endMsgReceived = false; // Used to know if to go to error screen when websocket closes
|
let endMsgReceived = false; // Used to know if to go to error screen when websocket closes
|
||||||
|
|
||||||
if (inputs.size <= 0) {
|
if (inputs.size <= 10) {
|
||||||
toast.show({ description: 'No MIDI Keyboard found' });
|
toast.show({ description: 'No MIDI Keyboard found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -238,14 +239,14 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
||||||
const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
setTime(() => getElapsedTime()); // Countdown
|
// setTime(() => getElapsedTime()); // Countdown
|
||||||
}, 200);
|
// }, 200);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ScreenOrientation.unlockAsync().catch(() => {});
|
ScreenOrientation.unlockAsync().catch(() => {});
|
||||||
stopwatch.stop();
|
// stopwatch.stop();
|
||||||
clearInterval(interval);
|
// clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -290,12 +291,13 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
|||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopupCC isVisible={endResult != undefined}>
|
<PlayEndModal />
|
||||||
|
{/* <PopupCC isVisible={endResult != undefined}>
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(() => (endResult ? <ScoreModal {...(endResult as any)} /> : <></>))()
|
endResult ? <ScoreModal {...(endResult as any)} /> : <></>
|
||||||
}
|
}
|
||||||
</PopupCC>
|
</PopupCC> */}
|
||||||
<PopupCC
|
<PopupCC
|
||||||
title={translate('selectPlayMode')}
|
title={translate('selectPlayMode')}
|
||||||
description={translate('selectPlayModeExplaination')}
|
description={translate('selectPlayModeExplaination')}
|
||||||
@@ -374,7 +376,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlayScore score={score} streak={streak} message={lastScoreMessage} />
|
<PlayScore />
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -386,38 +388,36 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PartitionMagic
|
<PartitionMagic
|
||||||
shouldPlay={shouldPlay}
|
|
||||||
timestamp={time}
|
|
||||||
songID={song.data.id}
|
songID={song.data.id}
|
||||||
onEndReached={() => {
|
// onEndReached={() => {
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
onEnd();
|
// onEnd();
|
||||||
}, 200);
|
// }, 200);
|
||||||
}}
|
// }}
|
||||||
onError={() => {
|
// onError={() => {
|
||||||
console.log('error from partition magic');
|
// console.log('error from partition magic');
|
||||||
}}
|
// }}
|
||||||
onReady={() => {
|
// onReady={() => {
|
||||||
console.log('ready from partition magic');
|
// console.log('ready from partition magic');
|
||||||
setReadyToPlay(true);
|
// setReadyToPlay(true);
|
||||||
}}
|
// }}
|
||||||
onPlay={onResume}
|
// onPlay={onResume}
|
||||||
onPause={onPause}
|
// onPause={onPause}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<PlayViewControlBar
|
<PlayViewControlBar
|
||||||
score={score}
|
// score={score}
|
||||||
time={time}
|
// time={time}
|
||||||
paused={paused}
|
// paused={paused}
|
||||||
disabled={playType == null || !readyToPlay}
|
// disabled={playType == null || !readyToPlay}
|
||||||
song={song.data}
|
song={song.data}
|
||||||
onEnd={onEnd}
|
// onEnd={onEnd}
|
||||||
onPause={() => {
|
// onPause={() => {
|
||||||
setShouldPlay(false);
|
// setShouldPlay(false);
|
||||||
}}
|
// }}
|
||||||
onResume={() => {
|
// onResume={() => {
|
||||||
setShouldPlay(true);
|
// setShouldPlay(true);
|
||||||
}}
|
// }}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
{colorScheme === 'dark' && (
|
{colorScheme === 'dark' && (
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ const ProfileView = () => {
|
|||||||
const isBigScreen = layout.width > 650;
|
const isBigScreen = layout.width > 650;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flex={1}>
|
<Flex
|
||||||
|
flex={1}
|
||||||
|
style={{
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -68,9 +73,15 @@ const ProfileView = () => {
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
gap: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text fontSize={'xl'} style={{ paddingRight: 'auto' }}>
|
<Text
|
||||||
|
fontSize={'xl'}
|
||||||
|
isTruncated
|
||||||
|
numberOfLines={2}
|
||||||
|
style={{ flexShrink: 1 }}
|
||||||
|
>
|
||||||
{userQuery.data.name}
|
{userQuery.data.name}
|
||||||
</Text>
|
</Text>
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
|
|||||||
@@ -7771,6 +7771,11 @@ join-component@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
||||||
integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==
|
integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==
|
||||||
|
|
||||||
|
jotai@^2.6.1:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.6.1.tgz#ece33a50b604e41b0134f94dd621e55d1bdc66f7"
|
||||||
|
integrity sha512-GLQtAnA9iEKRMXnyCjf1azIxfQi5JausX2EI5qSlb59j4i73ZEyV/EXPDEAQj4uQNZYEefi3degv/Pw3+L/Dtg==
|
||||||
|
|
||||||
js-sha3@0.8.0:
|
js-sha3@0.8.0:
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||||
|
|||||||
Reference in New Issue
Block a user