Merge remote-tracking branch 'origin/main' into feat/adc/search-view-v2

This commit is contained in:
Clément Le Bihan
2023-12-06 14:52:44 +01:00
14 changed files with 240 additions and 209 deletions

View File

@@ -15,7 +15,6 @@ import SettingsTab from './views/settings/SettingsView';
import { useQuery } from './Queries';
import API, { APIError } from './API';
import PlayView from './views/PlayView';
import ScoreView from './views/ScoreView';
import { LoadingView } from './components/Loading';
import ProfileView from './views/ProfileView';
import useColorScheme from './hooks/colorScheme';
@@ -79,11 +78,6 @@ const protectedRoutes = () =>
options: { title: translate('genreFilter') },
link: '/genre/:genreId',
},
Score: {
component: ScoreView,
options: { title: translate('score'), headerLeft: null },
link: undefined,
},
Search: {
component: SearchView,
options: { headerShown: false },

View File

@@ -0,0 +1,119 @@
import { Column, Row, Text, useTheme } from 'native-base';
import ButtonBase from './UI/ButtonBase';
import { Translate, TranslationKey, translate } from '../i18n/i18n';
import { Play, Star1 } from 'iconsax-react-native';
import { useNavigation } from '../Navigation';
import { StackActions } from '@react-navigation/native';
type ScoreModalProps = {
songId: number;
overallScore: number;
precision: number;
score: {
missed: number;
good: number;
great: number;
perfect: number;
wrong: number;
max_score: number;
current_streak: number;
max_streak: number;
};
};
const ScoreModal = (props: ScoreModalProps) => {
// const props = {
// songId: 1,
// overallScore: 74,
// precision: 0,
// score: {
// missed: 9,
// good: 1,
// great: 2,
// perfect: 4,
// wrong: 0,
// max_score: 100,
// current_streak: 1,
// max_streak: 11,
// } as const
// } as const; //TODO DELETE ME
const navigation = useNavigation();
const theme = useTheme();
const score = (props.overallScore * 100) / props.score.max_score;
const column1 = {
perfect: [props.score.perfect, 'primary'],
great: [props.score.great, 'secondary'],
good: [props.score.good, 'success'],
} as const;
const column2 = {
bestStreak: [props.score.max_streak, 'notification'],
missed: [props.score.missed, 'alert'],
wrong: [props.score.wrong, 'error'],
} as const;
return (
<Column w="xl" space={4} style={{ alignItems: 'center' }}>
<Row space={2} style={{ justifyContent: 'center' }}>
{[1, 2, 3].map((index) => (
<Star1
color={theme.colors.primary[500]}
key={index}
variant={score >= (index * 100) / 4 ? 'Bold' : 'Outline'}
/>
))}
</Row>
<Text fontSize="3xl">{Math.max(score, 0)}%</Text>
<Row w="100%" style={{ justifyContent: 'space-between' }}>
<Translate translationKey="precision" />
<Text>{props.precision}%</Text>
</Row>
<Row w="100%" space={2}>
{([column1, column2] as const).map((column, columnIndex) => (
<Column w="50%" space={2} key={columnIndex}>
{Object.entries(column).map(([key, [value, color]]) => {
const translationKey = key as TranslationKey;
return (
<Row
key={translationKey}
style={{ justifyContent: 'space-between' }}
>
<Translate
translationKey={translationKey}
fontWeight={'bold'}
color={`${color}.500`}
/>
<Text>x{value}</Text>
</Row>
);
})}
</Column>
))}
</Row>
<Row w="100%" style={{ justifyContent: 'space-between' }}>
<ButtonBase
style={{}}
icon={Play}
type="outlined"
title={translate('playAgain')}
onPress={() =>
navigation.dispatch(StackActions.replace('Play', { songId: props.songId }))
}
/>
<ButtonBase
style={{}}
icon={Play}
type="filled"
title={translate('menuMusic')}
onPress={() =>
navigation.canGoBack()
? navigation.goBack()
: navigation.navigate('HomeNew', {})
}
/>
</Row>
</Column>
);
};
export default ScoreModal;

View File

@@ -8,7 +8,7 @@ import React from 'react';
import GlassmorphismCC from './Glassmorphism';
type PopupCCProps = {
title: string;
title?: string;
description?: string;
children?: ReactNode;
isVisible: boolean;
@@ -53,17 +53,19 @@ const PopupCC = ({ title, description, children, isVisible, setIsVisible }: Popu
onPress={async () => setIsVisible(false)}
/>
)}
<Heading
size="md"
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Text style={{ flex: 1 }}>{title}</Text>
</Heading>
{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}
</View>

View File

@@ -7,6 +7,7 @@ import { Cup, Discover, Music, SearchNormal1, Setting2, User } from 'iconsax-rea
import { LoadingView } from '../Loading';
import ScaffoldDesktopCC from './ScaffoldDesktopCC';
import ScaffoldMobileCC from './ScaffoldMobileCC';
import { useAssets } from 'expo-asset';
const menu = [
{ type: 'main', title: 'menuDiscovery', icon: Discover, link: 'HomeNew' },
@@ -37,10 +38,11 @@ const ScaffoldCC = ({
return <LoadingView />;
}
const colorScheme = useColorScheme();
const logo =
const [logo] = useAssets(
colorScheme == 'light'
? require('../../assets/icon_light.png')
: require('../../assets/icon_dark.png');
: require('../../assets/icon_dark.png')
);
return (
<Flex style={{ flex: 1, backgroundColor: '#cdd4fd' }}>
@@ -48,7 +50,7 @@ const ScaffoldCC = ({
<ScaffoldMobileCC
enableScroll={enableScroll}
user={userQuery.data}
logo={logo}
logo={logo?.at(0)?.uri ?? ''}
routeName={routeName}
menu={menu}
widthPadding={withPadding}
@@ -58,7 +60,7 @@ const ScaffoldCC = ({
) : (
<ScaffoldDesktopCC
user={userQuery.data}
logo={logo}
logo={logo?.at(0)?.uri ?? ''}
routeName={routeName}
menu={menu}
widthPadding={withPadding}

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import { View, Image, TouchableOpacity } from 'react-native';
import { View, Image, Pressable } from 'react-native';
import { Divider, Text, ScrollView, Row, useMediaQuery, useTheme } from 'native-base';
import { useQuery } from '../../Queries';
import API from '../../API';
@@ -36,7 +36,21 @@ const SongHistory = (props: { quantity: number }) => {
return <LoadingView />;
}
const musics = history.data.map((h) => h.song)?.slice(0, props.quantity);
const musics = history.data
.reduce(
(acc, curr) => {
if (acc.length === 0) {
return [curr];
}
if (acc.find((h) => h.song!.id === curr.song!.id)) {
return acc;
}
return [...acc, curr];
},
[] as typeof history.data
)
.map((h) => h.song)
?.slice(0, props.quantity);
return (
<View>
@@ -45,18 +59,18 @@ const SongHistory = (props: { quantity: number }) => {
) : (
musics.map((song) => (
<View
key={'short-history-tab' + song.id}
key={'short-history-tab' + song!.id}
style={{
paddingHorizontal: 16,
paddingVertical: 10,
flex: 1,
}}
>
<TouchableOpacity
onPress={() => navigation.navigate('Play', { songId: song.id })}
<Pressable
onPress={() => navigation.navigate('Play', { songId: song!.id })}
>
<Text numberOfLines={1}>{song.name}</Text>
</TouchableOpacity>
<Text numberOfLines={1}>{song!.name}</Text>
</Pressable>
</View>
))
)}

View File

@@ -1,15 +1,32 @@
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
import { ModelValidator } from './Model';
import { SongValidator } from './Song';
export const SongHistoryItemValidator = yup.object({
songID: yup.number().required(),
song: SongValidator.required(),
userID: yup.number().required(),
score: yup.number().required(),
playDate: yup.date().required(),
difficulties: yup.mixed().required(),
});
export const SongHistoryItemValidator = yup
.object({
songID: yup.number().required(),
song: SongValidator.optional(),
userID: yup.number().required(),
info: yup
.object({
good: yup.number().required(),
great: yup.number().required(),
score: yup.number().required(),
wrong: yup.number().required(),
missed: yup.number().required(),
perfect: yup.number().required(),
max_score: yup.number().required(),
max_streak: yup.number().required(),
// there's also a current streak key but it doesn't
// conceptually makes sense outside of the played game
})
.required(),
score: yup.number().required(),
playDate: yup.date().required(),
difficulties: yup.mixed().required(),
})
.concat(ModelValidator);
export type SongHistoryItem = yup.InferType<typeof SongHistoryItemValidator>;
export const SongHistoryItemHandler: ResponseHandler<SongHistoryItem> = {

View File

@@ -59,10 +59,10 @@ const HomeView = (props: RouteProps<{}>) => {
playHistoryQuery.data
?.map((x) => x.song)
.map((song) => ({
cover: song.cover,
name: song.name,
songId: song.id,
artistName: song.artist!.name,
cover: song!.cover,
name: song!.name,
songId: song!.id,
artistName: song!.artist!.name,
})) ?? []
}
/>

View File

@@ -32,6 +32,7 @@ import PopupCC from '../components/UI/PopupCC';
import ButtonBase from '../components/UI/ButtonBase';
import { Clock, Cup } from 'iconsax-react-native';
import PlayViewControlBar from '../components/Play/PlayViewControlBar';
import ScoreModal from '../components/ScoreModal';
type PlayViewProps = {
songId: number;
@@ -80,10 +81,12 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
const [paused, setPause] = useState<boolean>(true);
const stopwatch = useStopwatch();
const [time, setTime] = useState(0);
const [endResult, setEndResult] = useState<unknown>();
const songHistory = useQuery(API.getSongHistory(songId));
const [score, setScore] = useState(0); // Between 0 and 100
// const fadeAnim = useRef(new Animated.Value(0)).current;
const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [midiKeyboardFound, setMidiKeyboardFound] = useState<boolean>();
// 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
@@ -183,11 +186,10 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
if (data.type == 'end') {
endMsgReceived = true;
webSocket.current?.close();
navigation.dispatch(
StackActions.replace('Score', { songId: song.data!.id, ...data })
);
setEndResult({ songId: song.data!.id, ...data });
return;
}
console.log(data);
const points = data.info.score;
const maxPoints = data.info.max_score || 1;
@@ -319,16 +321,52 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
zIndex: 100,
}}
>
<PopupCC isVisible={endResult != undefined}>
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(() => (endResult ? <ScoreModal {...(endResult as any)} /> : <></>))()
}
</PopupCC>
<PopupCC
title={translate('selectPlayMode')}
description={translate('selectPlayModeExplaination')}
isVisible={playType == null}
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>
{(
[
[
'lastScore',
songHistory.data?.best ?? 0,
songHistory.data?.history.at(0)?.score ?? 0,
() => <Clock color={statColor} />,
] as const,
[
'bestScore',
songHistory.data?.history.at(0)?.score ?? 0,
songHistory.data?.best ?? 0,
() => <Cup color={statColor} />,
],
] as const
@@ -435,7 +473,6 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
score={score}
time={time}
paused={paused}
disabled={!midiKeyboardFound}
song={song.data}
onEnd={onEnd}
onPause={onPause}

View File

@@ -1,160 +0,0 @@
import { Card, Column, Image, Row, Text, ScrollView, VStack } from 'native-base';
import Translate from '../components/Translate';
import { RouteProps, useNavigation } from '../Navigation';
import { CardBorderRadius } from '../components/Card';
import TextButton from '../components/TextButton';
import API from '../API';
import CardGridCustom from '../components/CardGridCustom';
import SongCard from '../components/SongCard';
import { useQuery } from '../Queries';
import { LoadingView } from '../components/Loading';
import ScoreGraph from '../components/ScoreGraph';
type ScoreViewProps = {
songId: number;
overallScore: number;
precision: number;
score: {
missed: number;
good: number;
great: number;
perfect: number;
wrong: number;
max_score: number;
current_streak: number;
max_streak: number;
};
};
const ScoreView = (props: RouteProps<ScoreViewProps>) => {
const { songId, overallScore, precision, score } = props;
const navigation = useNavigation();
const songQuery = useQuery(API.getSong(songId, ['artist']));
const recommendations = useQuery(API.getSongSuggestions(['artist']));
if (!recommendations.data || !songQuery.data) {
return <LoadingView />;
}
if (songQuery.isError) {
navigation.navigate('Error');
return <></>;
}
return (
<ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
<VStack width={{ base: '100%', lg: '50%' }} space={3} textAlign="center">
<Text bold fontSize="lg">
{songQuery.data.name}
</Text>
<Text bold>{songQuery.data.artist!.name}</Text>
<Row style={{ justifyContent: 'center', display: 'flex' }}>
<Card shadow={3} style={{ flex: 1 }}>
<Image
style={{
zIndex: 0,
aspectRatio: 1,
margin: 5,
borderRadius: CardBorderRadius,
}}
source={{ uri: songQuery.data.cover }}
/>
</Card>
<Card shadow={3} style={{ flex: 1 }}>
<Column style={{ justifyContent: 'space-evenly', flexGrow: 1 }}>
{/*<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
</Text>
<Translate translationKey='goodNotes' format={(t) => ' ' + t}/>
</Row>
<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
80
</Text>
<Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/>
</Row>*/}
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="score" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{overallScore + 'pts'}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="perfect" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{score.perfect}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="great" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{score.great}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="good" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{score.good}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="wrong" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{score.wrong}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="missed" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{score.missed}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="bestStreak" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{score.max_streak}
</Text>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey="precision" format={(t) => t + ' : '} />
<Text bold fontSize="xl">
{precision + '%'}
</Text>
</Row>
</Column>
</Card>
</Row>
<ScoreGraph />
<CardGridCustom
style={{ justifyContent: 'space-evenly' }}
content={recommendations.data.map((i) => ({
cover: i.cover,
name: i.name,
artistName: i.artist!.name,
songId: i.id,
}))}
cardComponent={SongCard}
heading={
<Text fontSize="sm">
<Translate translationKey="songsToGetBetter" />
</Text>
}
/>
<Row space={3} style={{ width: '100%', justifyContent: 'center' }}>
<TextButton
colorScheme="gray"
translate={{ translationKey: 'backBtn' }}
onPress={() => navigation.navigate('Home', {})}
/>
<TextButton
onPress={() => navigation.navigate('Play', { songId })}
translate={{ translationKey: 'playAgain' }}
/>
</Row>
</VStack>
</ScrollView>
);
};
export default ScoreView;