Merge remote-tracking branch 'origin/main' into feat/adc/search-view-v2
This commit is contained in:
@@ -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 },
|
||||
|
||||
119
front/components/ScoreModal.tsx
Normal file
119
front/components/ScoreModal.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user