started to implement jotai states to control and manage the playview
This commit is contained in:
@@ -8,20 +8,16 @@ import { Audio } from 'expo-av';
|
||||
import { SvgContainer } from './SvgContainer';
|
||||
import LoadingComponent from '../Loading';
|
||||
import { SplendidGrandPiano } from 'smplr';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
|
||||
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 = {
|
||||
timestamp: 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 = (
|
||||
@@ -43,16 +39,7 @@ const getCursorToPlay = (
|
||||
|
||||
const transitionDuration = 50;
|
||||
|
||||
const PartitionMagic = ({
|
||||
timestamp,
|
||||
songID,
|
||||
shouldPlay,
|
||||
onEndReached,
|
||||
onError,
|
||||
onReady,
|
||||
onPlay,
|
||||
onPause,
|
||||
}: ParitionMagicProps) => {
|
||||
const PartitionMagic = ({ songID }: ParitionMagicProps) => {
|
||||
const { data, isLoading, isError } = useQuery(API.getSongCursorInfos(songID));
|
||||
const currentCurIdx = React.useRef(-1);
|
||||
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
||||
@@ -61,6 +48,9 @@ const PartitionMagic = ({
|
||||
const melodySound = React.useRef<Audio.Sound | null>(null);
|
||||
const piano = React.useRef<SplendidGrandPiano | null>(null);
|
||||
const [isPianoLoaded, setIsPianoLoaded] = React.useState(false);
|
||||
const timestamp = useAtom(timestampAtom)[0];
|
||||
const shouldPlay = useAtom(shouldPlayAtom)[0];
|
||||
const [, setPartitionState] = useAtom(partitionStateAtom);
|
||||
const cursorPaddingVertical = 10;
|
||||
const cursorPaddingHorizontal = 3;
|
||||
|
||||
@@ -75,7 +65,8 @@ const PartitionMagic = ({
|
||||
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);
|
||||
melodySound.current?.pauseAsync();
|
||||
setPartitionState('ended');
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -115,15 +106,14 @@ const PartitionMagic = ({
|
||||
}, [data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onError && isError) {
|
||||
onError('Error while loading partition');
|
||||
return;
|
||||
if (isError) {
|
||||
setPartitionState('error');
|
||||
}
|
||||
}, [onError, isError]);
|
||||
}, [isError]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isPartitionSvgLoaded && !isLoading && (melodySound.current?._loaded || isPianoLoaded)) {
|
||||
onReady();
|
||||
setPartitionState('ready');
|
||||
}
|
||||
}, [isPartitionSvgLoaded, isLoading, melodySound.current?._loaded, isPianoLoaded]);
|
||||
|
||||
@@ -132,27 +122,29 @@ const PartitionMagic = ({
|
||||
if (!piano.current || !isPianoLoaded) {
|
||||
return;
|
||||
}
|
||||
shouldPlay ? onPlay() : onPause();
|
||||
setPartitionState(shouldPlay ? 'playing' : 'paused');
|
||||
return;
|
||||
}
|
||||
if (!melodySound.current || !melodySound.current._loaded) {
|
||||
return;
|
||||
}
|
||||
if (shouldPlay) {
|
||||
melodySound.current.playAsync().then(onPlay).catch(console.error);
|
||||
melodySound.current
|
||||
.playAsync()
|
||||
.then(() => {
|
||||
setPartitionState('playing');
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
melodySound.current.pauseAsync().then(onPause).catch(console.error);
|
||||
melodySound.current
|
||||
.pauseAsync()
|
||||
.then(() => {
|
||||
setPartitionState('paused');
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}, [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;
|
||||
@@ -249,7 +241,7 @@ const PartitionMagic = ({
|
||||
}}
|
||||
>
|
||||
<SvgContainer
|
||||
url={getSVGURL(songID)}
|
||||
url={API.getPartitionSvgUrl(songID)}
|
||||
onReady={() => {
|
||||
setIsPartitionSvgLoaded(true);
|
||||
}}
|
||||
@@ -277,11 +269,4 @@ const PartitionMagic = ({
|
||||
);
|
||||
};
|
||||
|
||||
PartitionMagic.defaultProps = {
|
||||
onError: () => {},
|
||||
onReady: () => {},
|
||||
onPlay: () => {},
|
||||
onPause: () => {},
|
||||
};
|
||||
|
||||
export default PartitionMagic;
|
||||
|
||||
31
front/components/Play/PartitionTimestampText.tsx
Normal file
31
front/components/Play/PartitionTimestampText.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -10,20 +10,21 @@ import Animated, {
|
||||
Easing,
|
||||
} from 'react-native-reanimated';
|
||||
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 = {
|
||||
content: string;
|
||||
color?: ColorSchemeType;
|
||||
id: number;
|
||||
};
|
||||
|
||||
type PlayScoreProps = {
|
||||
score: number;
|
||||
timestamp: 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);
|
||||
// this style should bounce in on enter and fade away
|
||||
const scoreMsgStyle = useAnimatedStyle(() => {
|
||||
@@ -92,9 +93,9 @@ export const PlayScore = ({ score, streak, message }: PlayScoreProps) => {
|
||||
<Text color={textColor[900]} fontSize={20}>
|
||||
{message.content}
|
||||
</Text>
|
||||
{streak > 0 && (
|
||||
{message.streak > 0 && (
|
||||
<Text color={textColor[900]} fontSize={15} bold>
|
||||
{`x${streak}`}
|
||||
{`x${message.streak}`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -6,33 +6,27 @@ import { MetronomeControls } from '../Metronome';
|
||||
import StarProgress from '../StarProgress';
|
||||
import Song from '../../models/Song';
|
||||
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 = {
|
||||
song: Song;
|
||||
time: number;
|
||||
paused: boolean;
|
||||
score: number;
|
||||
disabled: boolean;
|
||||
onResume: () => void;
|
||||
onPause: () => void;
|
||||
onEnd: () => void;
|
||||
};
|
||||
|
||||
const PlayViewControlBar = ({
|
||||
song,
|
||||
time,
|
||||
paused,
|
||||
score,
|
||||
disabled,
|
||||
onResume,
|
||||
onPause,
|
||||
onEnd,
|
||||
}: PlayViewControlBarProps) => {
|
||||
const PlayViewControlBar = ({ song }: PlayViewControlBarProps) => {
|
||||
const [, setShouldPlay] = useAtom(shouldPlayAtom);
|
||||
const [partitionState] = useAtom(partitionStateAtom);
|
||||
const [, setShouldEnd] = useAtom(shouldEndAtom);
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const bpm = React.useRef<number>(60);
|
||||
const { colors } = useTheme();
|
||||
const textColor = colors.text;
|
||||
const paused = partitionState === 'paused';
|
||||
const disabled = partitionState === 'loading' || partitionState === 'error';
|
||||
return (
|
||||
<Row
|
||||
style={{
|
||||
@@ -116,7 +110,7 @@ const PlayViewControlBar = ({
|
||||
color: colors.coolGray[900],
|
||||
name: paused ? 'play' : 'pause',
|
||||
}}
|
||||
onPress={paused ? onResume : onPause}
|
||||
onPress={() => setShouldPlay(paused)}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
@@ -126,22 +120,10 @@ const PlayViewControlBar = ({
|
||||
as: Ionicons,
|
||||
name: 'stop',
|
||||
}}
|
||||
onPress={onEnd}
|
||||
onPress={() => setShouldEnd(true)}
|
||||
/>
|
||||
<Text color={textColor[900]}>
|
||||
{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>
|
||||
<TimestampDisplay />
|
||||
<StarProgress
|
||||
value={score}
|
||||
max={100}
|
||||
starSteps={[50, 75, 90]}
|
||||
style={{
|
||||
@@ -168,11 +150,4 @@ const PlayViewControlBar = ({
|
||||
);
|
||||
};
|
||||
|
||||
PlayViewControlBar.defaultProps = {
|
||||
onResume: () => {},
|
||||
onPause: () => {},
|
||||
onEnd: () => {},
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default PlayViewControlBar;
|
||||
|
||||
@@ -2,15 +2,17 @@ import * as React from 'react';
|
||||
import { Progress } from 'native-base';
|
||||
import { View, ViewStyle, StyleProp } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { scoreAtom } from './Play/PlayScore';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
export interface StarProgressProps {
|
||||
value: number;
|
||||
max: number;
|
||||
starSteps: number[];
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const StarProgress = (props: StarProgressProps) => {
|
||||
const [score] = useAtom(scoreAtom);
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -29,15 +31,15 @@ const StarProgress = (props: StarProgressProps) => {
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
value={props.value}
|
||||
value={score}
|
||||
max={props.max}
|
||||
/>
|
||||
{props.starSteps.map((step) => {
|
||||
return (
|
||||
<Ionicons
|
||||
key={step}
|
||||
name={step <= props.value ? 'star' : 'star-outline'}
|
||||
color={step <= props.value ? '#EBDA3C' : '#6075F9'}
|
||||
name={step <= score ? 'star' : 'star-outline'}
|
||||
color={step <= score ? '#EBDA3C' : '#6075F9'}
|
||||
size={20}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"fbjs": "^3.0.5",
|
||||
"i18next": "^23.5.1",
|
||||
"iconsax-react-native": "^0.0.8",
|
||||
"jotai": "^2.6.1",
|
||||
"native-base": "^3.4.28",
|
||||
"normalize-css-color": "^1.0.2",
|
||||
"react": "18.2.0",
|
||||
|
||||
@@ -374,7 +374,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
<PlayScore score={score} streak={streak} message={lastScoreMessage} />
|
||||
<PlayScore />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -386,23 +386,21 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
}}
|
||||
>
|
||||
<PartitionMagic
|
||||
shouldPlay={shouldPlay}
|
||||
timestamp={time}
|
||||
songID={song.data.id}
|
||||
onEndReached={() => {
|
||||
setTimeout(() => {
|
||||
onEnd();
|
||||
}, 200);
|
||||
}}
|
||||
onError={() => {
|
||||
console.log('error from partition magic');
|
||||
}}
|
||||
onReady={() => {
|
||||
console.log('ready from partition magic');
|
||||
setReadyToPlay(true);
|
||||
}}
|
||||
onPlay={onResume}
|
||||
onPause={onPause}
|
||||
// onEndReached={() => {
|
||||
// setTimeout(() => {
|
||||
// onEnd();
|
||||
// }, 200);
|
||||
// }}
|
||||
// onError={() => {
|
||||
// console.log('error from partition magic');
|
||||
// }}
|
||||
// onReady={() => {
|
||||
// console.log('ready from partition magic');
|
||||
// setReadyToPlay(true);
|
||||
// }}
|
||||
// onPlay={onResume}
|
||||
// onPause={onPause}
|
||||
/>
|
||||
</View>
|
||||
<PlayViewControlBar
|
||||
|
||||
@@ -7771,6 +7771,11 @@ join-component@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
||||
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:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
|
||||
Reference in New Issue
Block a user