Compare commits
68 Commits
clem-fixes
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f804bb9a | ||
|
|
0b78772d0b | ||
|
|
9e3c2d1cca | ||
|
|
1b097163a4 | ||
|
|
c61d17baa7 | ||
|
|
be8867e12f | ||
|
|
00f98151c1 | ||
|
|
5d78d8b5dd | ||
|
|
0bd12bbf34 | ||
|
|
88cb7b2b65 | ||
|
|
69329118f7 | ||
|
|
2781276c12 | ||
|
|
a24a960184 | ||
|
|
9fd70d3110 | ||
|
|
c1d714e02a | ||
|
|
c08a1a2c74 | ||
|
|
23a1ff8d19 | ||
|
|
b80167001f | ||
|
|
8c2a53aa41 | ||
|
|
dcca780f2d | ||
|
|
9150817c05 | ||
|
|
d57606dd53 | ||
|
|
52f2c94fb7 | ||
|
|
1952625098 | ||
|
|
10dbfda8a4 | ||
|
|
234335cf61 | ||
|
|
52d40b43f0 | ||
|
|
50522bbe63 | ||
|
|
ce927ea1a4 | ||
|
|
aebf409cea | ||
|
|
5f0ea41c04 | ||
|
|
d3c7e4a0a1 | ||
|
|
a3893bdb2b | ||
|
|
4ba4303b1e | ||
| e779876f54 | |||
| bd9edaa60e | |||
|
|
f2ad34c8ab | ||
|
|
131d7bf688 | ||
|
|
38110d2840 | ||
|
|
fd60f2d171 | ||
|
|
86b2c1be50 | ||
|
|
627b8df658 | ||
|
|
3f0d0d523b | ||
|
|
29a9ffce74 | ||
|
|
a69e5ac009 | ||
|
|
caa3322676 | ||
|
|
934010a0c1 | ||
|
|
29b2bedae0 | ||
|
|
7a2b877714 | ||
|
|
9416393618 | ||
|
|
eb245118dc | ||
|
|
40f16ab9ca | ||
|
|
a33d56bd61 | ||
|
|
c7c9250594 | ||
|
|
1b1659fe92 | ||
|
|
3c9d71a757 | ||
|
|
342099157e | ||
|
|
bb7a17fc22 | ||
|
|
0ea8cb86bb | ||
|
|
90f9574a6f | ||
|
|
f2f7ec3f8d | ||
|
|
88b111529b | ||
|
|
5fc937d81b | ||
|
|
b3853646cb | ||
|
|
dac9849ef5 | ||
|
|
11ed8f90fd | ||
|
|
5d103c6687 | ||
|
|
be926dcaed |
@@ -1,3 +1,3 @@
|
||||
FROM node:17
|
||||
FROM node:18.10.0
|
||||
WORKDIR /app
|
||||
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||
|
||||
10761
back/package-lock.json
generated
10761
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { Controller, Get, Put } from "@nestjs/common";
|
||||
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ScoresService } from "./scores.service";
|
||||
import { User } from "@prisma/client";
|
||||
@@ -13,4 +13,10 @@ export class ScoresController {
|
||||
getTopTwenty(): Promise<User[]> {
|
||||
return this.scoresService.topTwenty();
|
||||
}
|
||||
|
||||
// @ApiOkResponse{{description: "Successfully updated the user's total score"}}
|
||||
// @Put("/add")
|
||||
// addScore(): Promise<void> {
|
||||
// return this.ScoresService.add()
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ import { ArtistController } from "src/artist/artist.controller";
|
||||
export class SearchController {
|
||||
constructor(private readonly searchService: SearchService) {}
|
||||
|
||||
@Get("songs/:query")
|
||||
@Get("songs")
|
||||
@ApiOkResponse({ type: _Song, isArray: true })
|
||||
@ApiOperation({ description: "Search a song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async searchSong(
|
||||
@Request() req: any,
|
||||
@Query("q") query: string | null,
|
||||
@Query("artistId") artistId: number,
|
||||
@Query("genreId") genreId: number,
|
||||
@Query("artistId", new ParseIntPipe({ optional: true })) artistId: number,
|
||||
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@@ -51,7 +51,7 @@ export class SearchController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("artists/:query")
|
||||
@Get("artists")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
|
||||
@@ -5,7 +5,6 @@ volumes:
|
||||
scoro_logs:
|
||||
meilisearch:
|
||||
|
||||
|
||||
services:
|
||||
back:
|
||||
#platform: linux/amd64
|
||||
@@ -93,7 +92,7 @@ services:
|
||||
- "4567:4567"
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.4
|
||||
image: getmeili/meilisearch:v1.5
|
||||
volumes:
|
||||
- meilisearch:/meili_data
|
||||
env_file:
|
||||
@@ -104,4 +103,3 @@ services:
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
|
||||
103
front/API.ts
103
front/API.ts
@@ -1,5 +1,4 @@
|
||||
import Artist, { ArtistHandler } from './models/Artist';
|
||||
import Album from './models/Album';
|
||||
import Chapter from './models/Chapter';
|
||||
import Lesson from './models/Lesson';
|
||||
import Genre, { GenreHandler } from './models/Genre';
|
||||
@@ -24,6 +23,7 @@ import * as yup from 'yup';
|
||||
import { base64ToBlob } from './utils/base64ToBlob';
|
||||
import { ImagePickerAsset } from 'expo-image-picker';
|
||||
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
||||
import { searchProps } from './views/V2/SearchView';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
type RegistrationInput = AuthenticationInput & { email: string };
|
||||
@@ -497,84 +497,6 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a song by its name
|
||||
* @param query the string used to find the songs
|
||||
*/
|
||||
public static searchSongs(query: string): Query<Song[]> {
|
||||
return {
|
||||
key: ['search', 'song', query],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/songs/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search artists by name
|
||||
* @param query the string used to find the artists
|
||||
*/
|
||||
public static searchArtists(query: string): Query<Artist[]> {
|
||||
return {
|
||||
key: ['search', 'artist', query],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/artists/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(ArtistHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Album by name
|
||||
* @param query the string used to find the album
|
||||
*/
|
||||
public static searchAlbum(query: string): Query<Album[]> {
|
||||
return {
|
||||
key: ['search', 'album', query],
|
||||
exec: async () => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Trooper',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kingdom Heart 365/2 OST',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Random Access Memories',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve music genres
|
||||
*/
|
||||
public static searchGenres(query: string): Query<Genre[]> {
|
||||
return {
|
||||
key: ['search', 'genre', query],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/genres/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(GenreHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a lesson
|
||||
* @param lessonId the id to find the lesson
|
||||
@@ -780,6 +702,29 @@ export default class API {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
|
||||
public static searchSongs(query: searchProps, include?: SongInclude[]): Query<Song[]> {
|
||||
const queryParams: string[] = [];
|
||||
|
||||
if (query.query) queryParams.push(`q=${encodeURIComponent(query.query)}`);
|
||||
if (query.artist) queryParams.push(`artistId=${query.artist}`);
|
||||
if (query.genre) queryParams.push(`genreId=${query.genre}`);
|
||||
if (include) queryParams.push(`include=${include.join(',')}`);
|
||||
|
||||
const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||
|
||||
return {
|
||||
key: ['search', query.query, query.artist, query.genre, include],
|
||||
exec: () => {
|
||||
return API.fetch(
|
||||
{
|
||||
route: `/search/songs${queryString}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHandler) }
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getPartitionMelodyUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/melody`;
|
||||
}
|
||||
|
||||
@@ -30,9 +30,8 @@ const phoneLightGlassmorphism = {
|
||||
900: 'rgb(248, 250, 254)',
|
||||
1000: 'rgb(252, 254, 254)',
|
||||
};
|
||||
const lightGlassmorphism =
|
||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
||||
const darkGlassmorphism = {
|
||||
|
||||
const defaultDarkGlassmorphism = {
|
||||
50: 'rgba(16,16,20,0.9)',
|
||||
100: 'rgba(16,16,20,0.1)',
|
||||
200: 'rgba(16,16,20,0.2)',
|
||||
@@ -46,6 +45,24 @@ const darkGlassmorphism = {
|
||||
1000: 'rgba(16,16,20,1)',
|
||||
};
|
||||
|
||||
const phoneDarkGlassmorphism = {
|
||||
50: 'rgb(10, 14, 38)',
|
||||
100: 'rgb(14, 18, 42)',
|
||||
200: 'rgb(18, 22, 46)',
|
||||
300: 'rgb(22, 26, 50)',
|
||||
400: 'rgb(26, 30, 54)',
|
||||
500: 'rgb(10, 20, 54)',
|
||||
600: 'rgb(14, 24, 58)',
|
||||
700: 'rgb(18, 28, 62)',
|
||||
800: 'rgb(22, 32, 66)',
|
||||
900: 'rgb(26, 36, 70)',
|
||||
1000: 'rgb(30, 40, 74)',
|
||||
};
|
||||
|
||||
const lightGlassmorphism =
|
||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
||||
const darkGlassmorphism = Platform.OS === 'web' ? defaultDarkGlassmorphism : phoneDarkGlassmorphism;
|
||||
|
||||
const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
|
||||
@@ -2,7 +2,12 @@ import * as React from 'react';
|
||||
import { Platform, View } from 'react-native';
|
||||
import API from '../../API';
|
||||
import { useQuery } from '../../Queries';
|
||||
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated';
|
||||
import { CursorInfoItem } from '../../models/SongCursorInfos';
|
||||
import { Audio } from 'expo-av';
|
||||
import { SvgContainer } from './SvgContainer';
|
||||
@@ -10,7 +15,8 @@ import LoadingComponent from '../Loading';
|
||||
import { SplendidGrandPiano } from 'smplr';
|
||||
|
||||
export type ParitionMagicProps = {
|
||||
timestamp: number;
|
||||
playType: 'practice' | 'normal' | null;
|
||||
timestamp: React.MutableRefObject<number>;
|
||||
songID: number;
|
||||
shouldPlay: boolean;
|
||||
onEndReached: () => void;
|
||||
@@ -34,16 +40,18 @@ const getCursorToPlay = (
|
||||
return;
|
||||
}
|
||||
for (let i = cursorInfos.length - 1; i > currentCurIdx; i--) {
|
||||
const cursorInfo = cursorInfos[i]!;
|
||||
if (cursorInfo.timestamp <= timestamp) {
|
||||
onCursorMove(cursorInfo, i);
|
||||
if (cursorInfos[i]!.timestamp <= timestamp) {
|
||||
return onCursorMove(cursorInfos[i]!, i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const transitionDuration = 50;
|
||||
|
||||
export let updateCursor: (() => void) | undefined = undefined;
|
||||
|
||||
const PartitionMagic = ({
|
||||
playType,
|
||||
timestamp,
|
||||
songID,
|
||||
shouldPlay,
|
||||
@@ -72,6 +80,37 @@ const PartitionMagic = ({
|
||||
const cursorTop = (data?.cursors[cursorDisplayIdx]?.y ?? 0) - cursorPaddingVertical;
|
||||
const cursorLeft = (data?.cursors[0]?.x ?? 0) - cursorPaddingHorizontal;
|
||||
|
||||
updateCursor = () => {
|
||||
if (!shouldPlay) return;
|
||||
if (!data || data?.cursors.length === 0) return;
|
||||
getCursorToPlay(
|
||||
data!.cursors,
|
||||
currentCurIdx.current,
|
||||
timestamp.current + transitionDuration,
|
||||
(cursor, idx) => {
|
||||
currentCurIdx.current = idx;
|
||||
partitionOffset.value = withTiming(
|
||||
-(cursor.x - data!.cursors[0]!.x) / partitionDims[0],
|
||||
{
|
||||
duration: transitionDuration,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
}
|
||||
);
|
||||
if (idx === data!.cursors.length - 1) {
|
||||
setEndPartitionReached(true);
|
||||
}
|
||||
if (playType === 'practice') return;
|
||||
if (!isPianoLoaded) return;
|
||||
cursor.notes.forEach((note) => {
|
||||
piano.current?.start({
|
||||
note: note.note,
|
||||
duration: note.duration,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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 :(
|
||||
@@ -79,6 +118,11 @@ const PartitionMagic = ({
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
// In practice mode, no sound is played so just act as the piano is loaded
|
||||
if (playType === 'practice' || !playType) {
|
||||
setIsPianoLoaded(true);
|
||||
return;
|
||||
}
|
||||
if (Platform.OS === 'web' && !piano.current) {
|
||||
const audio = new AudioContext();
|
||||
piano.current = new SplendidGrandPiano(audio);
|
||||
@@ -109,7 +153,7 @@ const PartitionMagic = ({
|
||||
piano.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [playType]);
|
||||
const partitionDims = React.useMemo<[number, number]>(() => {
|
||||
return [data?.pageWidth ?? 0, data?.pageHeight ?? 1];
|
||||
}, [data]);
|
||||
@@ -146,65 +190,18 @@ const PartitionMagic = ({
|
||||
}, [shouldPlay]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (endPartitionReached) {
|
||||
if (endPartitionReached && playType === 'normal') {
|
||||
// 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(() => {
|
||||
if (!shouldPlay) return;
|
||||
if (!piano.current || !isPianoLoaded) return;
|
||||
if (!data || data?.cursors.length === 0) return;
|
||||
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),
|
||||
}
|
||||
);
|
||||
cursor.notes.forEach((note) => {
|
||||
piano.current?.start({
|
||||
note: note.note,
|
||||
duration: note.duration,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}, [timestamp, data?.cursors, isPianoLoaded]);
|
||||
React.useEffect(updateCursor, [data?.cursors, isPianoLoaded, timestamp.current]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
left: `${partitionOffset.value * 100}%`,
|
||||
}));
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -238,15 +235,17 @@ const PartitionMagic = ({
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
aspectRatio: partitionDims[0] / partitionDims[1],
|
||||
left: `${partitionOffset.value * 100}%`,
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SvgContainer
|
||||
url={getSVGURL(songID)}
|
||||
|
||||
35
front/components/Play/PlayTimestampShow.tsx
Normal file
35
front/components/Play/PlayTimestampShow.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Text, useTheme } from 'native-base';
|
||||
import { MutableRefObject, useEffect, useState } from 'react';
|
||||
|
||||
export let updateTime: (() => void) | undefined = undefined;
|
||||
|
||||
type PlayTimestampShowProps = {
|
||||
paused: boolean;
|
||||
time: MutableRefObject<number>;
|
||||
};
|
||||
|
||||
export const PlayTimestampShow = ({ paused, time }: PlayTimestampShowProps) => {
|
||||
const { colors } = useTheme();
|
||||
const textColor = colors.text;
|
||||
const [timeD, setTimeD] = useState<number>(time.current);
|
||||
|
||||
updateTime = () => {
|
||||
setTimeD(time.current);
|
||||
};
|
||||
|
||||
useEffect(updateTime, [time]);
|
||||
return (
|
||||
<Text color={textColor[900]}>
|
||||
{timeD < 0
|
||||
? paused
|
||||
? '0:00'
|
||||
: Math.floor((timeD % 60000) / 1000)
|
||||
.toFixed(0)
|
||||
.toString()
|
||||
: `${Math.floor(timeD / 60000)}:${Math.floor((timeD % 60000) / 1000)
|
||||
.toFixed(0)
|
||||
.toString()
|
||||
.padStart(2, '0')}`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -6,10 +6,12 @@ import { MetronomeControls } from '../Metronome';
|
||||
import StarProgress from '../StarProgress';
|
||||
import Song from '../../models/Song';
|
||||
import { useTheme } from 'native-base';
|
||||
import { PlayTimestampShow } from './PlayTimestampShow';
|
||||
|
||||
type PlayViewControlBarProps = {
|
||||
playType: 'practice' | 'normal' | null;
|
||||
song: Song;
|
||||
time: number;
|
||||
time: React.MutableRefObject<number>;
|
||||
paused: boolean;
|
||||
score: number;
|
||||
disabled: boolean;
|
||||
@@ -19,6 +21,7 @@ type PlayViewControlBarProps = {
|
||||
};
|
||||
|
||||
const PlayViewControlBar = ({
|
||||
playType,
|
||||
song,
|
||||
time,
|
||||
paused,
|
||||
@@ -107,6 +110,7 @@ const PlayViewControlBar = ({
|
||||
gap: isPhone ? 10 : 25,
|
||||
}}
|
||||
>
|
||||
{playType != 'practice' && (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="solid"
|
||||
@@ -118,6 +122,7 @@ const PlayViewControlBar = ({
|
||||
}}
|
||||
onPress={paused ? onResume : onPause}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
size="sm"
|
||||
colorScheme="coolGray"
|
||||
@@ -128,18 +133,7 @@ const PlayViewControlBar = ({
|
||||
}}
|
||||
onPress={onEnd}
|
||||
/>
|
||||
<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>
|
||||
<PlayTimestampShow paused={paused} time={time} />
|
||||
<StarProgress
|
||||
value={score}
|
||||
max={100}
|
||||
@@ -162,7 +156,7 @@ const PlayViewControlBar = ({
|
||||
minWidth: 120,
|
||||
}}
|
||||
>
|
||||
<MetronomeControls paused={paused} bpm={bpm.current} />
|
||||
{playType != 'practice' && <MetronomeControls paused={paused} bpm={bpm.current} />}
|
||||
</View>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -121,7 +121,7 @@ const Graph = ({ songId, since }: GraphProps) => {
|
||||
|
||||
const ScoreGraph = () => {
|
||||
const layout = useWindowDimensions();
|
||||
const songs = useQuery(API.getAllSongs);
|
||||
const songs = useQuery(API.getAllSongs());
|
||||
const rangeOptions = [
|
||||
{ label: '3 derniers jours', value: '3days' },
|
||||
{ label: 'Dernière semaine', value: 'week' },
|
||||
|
||||
@@ -46,7 +46,7 @@ const ScoreModal = (props: ScoreModalProps) => {
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
<Text fontSize="3xl">{Math.max(score, 0)}%</Text>
|
||||
<Text fontSize="3xl">{Math.max(score, 0).toFixed(2)}%</Text>
|
||||
<Row w="100%" style={{ justifyContent: 'space-between' }}>
|
||||
<Translate translationKey="precision" />
|
||||
<Text>{props.precision}%</Text>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Icon, Input, Button, Flex } from 'native-base';
|
||||
import React from 'react';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import { SearchContext } from '../views/SearchView';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export type Filter = 'artist' | 'song' | 'genre' | 'all' | 'favorites';
|
||||
|
||||
type FilterButton = {
|
||||
name: string;
|
||||
callback: () => void;
|
||||
id: Filter;
|
||||
};
|
||||
|
||||
const SearchBar = () => {
|
||||
const { filter, updateFilter } = React.useContext(SearchContext);
|
||||
const { stringQuery, updateStringQuery } = React.useContext(SearchContext);
|
||||
const [barText, updateBarText] = React.useState(stringQuery);
|
||||
|
||||
const debouncedUpdateStringQuery = debounce(updateStringQuery, 500);
|
||||
|
||||
// there's a bug due to recursive feedback that erase the text as soon as you type this is a temporary "fix"
|
||||
// will probably be fixed by removing the React.useContext
|
||||
// React.useEffect(() => {
|
||||
// updateBarText(stringQuery);
|
||||
// }, [stringQuery]);
|
||||
|
||||
const handleClearQuery = () => {
|
||||
updateStringQuery('');
|
||||
updateBarText('');
|
||||
};
|
||||
|
||||
const handleChangeText = (text: string) => {
|
||||
debouncedUpdateStringQuery(text);
|
||||
updateBarText(text);
|
||||
};
|
||||
|
||||
const filters: FilterButton[] = [
|
||||
{
|
||||
name: translate('allFilter'),
|
||||
callback: () => updateFilter('all'),
|
||||
id: 'all',
|
||||
},
|
||||
{
|
||||
name: translate('favoriteFilter'),
|
||||
callback: () => updateFilter('favorites'),
|
||||
id: 'favorites',
|
||||
},
|
||||
{
|
||||
name: translate('artistFilter'),
|
||||
callback: () => updateFilter('artist'),
|
||||
id: 'artist',
|
||||
},
|
||||
{
|
||||
name: translate('songsFilter'),
|
||||
callback: () => updateFilter('song'),
|
||||
id: 'song',
|
||||
},
|
||||
{
|
||||
name: translate('genreFilter'),
|
||||
callback: () => updateFilter('genre'),
|
||||
id: 'genre',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex m={3} flexDirection={['column', 'row']}>
|
||||
<Input
|
||||
onChangeText={(text) => handleChangeText(text)}
|
||||
variant={'rounded'}
|
||||
value={barText}
|
||||
rounded={'full'}
|
||||
placeholder={translate('search')}
|
||||
width={['100%', '50%']} //responsive array syntax with native-base
|
||||
py={2}
|
||||
px={2}
|
||||
fontSize={'12'}
|
||||
InputLeftElement={
|
||||
<Icon
|
||||
m={[1, 2]}
|
||||
ml={[2, 3]}
|
||||
size={['4', '6']}
|
||||
color="gray.400"
|
||||
as={<MaterialIcons name="search" />}
|
||||
/>
|
||||
}
|
||||
InputRightElement={
|
||||
<Icon
|
||||
m={[1, 2]}
|
||||
mr={[2, 3]}
|
||||
size={['4', '6']}
|
||||
color="gray.400"
|
||||
onPress={handleClearQuery}
|
||||
as={<MaterialIcons name="close" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Flex flexDirection={'row'}>
|
||||
{filters.map((btn) => (
|
||||
<Button
|
||||
key={btn.name}
|
||||
rounded={'full'}
|
||||
onPress={btn.callback}
|
||||
mx={[2, 5]}
|
||||
my={[1, 0]}
|
||||
minW={[30, 20]}
|
||||
variant={filter === btn.id ? 'solid' : 'outline'}
|
||||
>
|
||||
{btn.name}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
@@ -1,277 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Heading,
|
||||
Box,
|
||||
Card,
|
||||
Flex,
|
||||
useBreakpointValue,
|
||||
Column,
|
||||
ScrollView,
|
||||
} from 'native-base';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { SearchContext } from '../views/SearchView';
|
||||
import { useQuery } from '../Queries';
|
||||
import { Translate, translate } from '../i18n/i18n';
|
||||
import API from '../API';
|
||||
import LoadingComponent, { LoadingView } from './Loading';
|
||||
import ArtistCard from './ArtistCard';
|
||||
import GenreCard from './GenreCard';
|
||||
import SongCard from './SongCard';
|
||||
import CardGridCustom from './CardGridCustom';
|
||||
import SearchHistoryCard from './HistoryCard';
|
||||
import Song from '../models/Song';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import SongRow from '../components/SongRow';
|
||||
import FavSongRow from './FavSongRow';
|
||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||
|
||||
const swaToSongCardProps = (song: Song) => ({
|
||||
songId: song.id,
|
||||
name: song.name,
|
||||
artistName: song.artist!.name,
|
||||
cover: song.cover,
|
||||
});
|
||||
|
||||
const HomeSearchComponent = () => {
|
||||
const { updateStringQuery } = React.useContext(SearchContext);
|
||||
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
|
||||
API.getSearchHistory(0, 12),
|
||||
{ enabled: true }
|
||||
);
|
||||
const songSuggestions = useQuery(API.getSongSuggestions(['artist']));
|
||||
|
||||
return (
|
||||
<VStack mt="5" style={{ overflow: 'hidden' }}>
|
||||
<Card shadow={3} mb={5}>
|
||||
<Heading margin={5}>{translate('lastSearched')}</Heading>
|
||||
{isLoadingHistory ? (
|
||||
<LoadingComponent />
|
||||
) : (
|
||||
<CardGridCustom
|
||||
content={historyData.map((h) => {
|
||||
return {
|
||||
...h,
|
||||
timestamp: h.timestamp.toLocaleString(),
|
||||
onPress: () => {
|
||||
updateStringQuery(h.query);
|
||||
},
|
||||
};
|
||||
})}
|
||||
cardComponent={SearchHistoryCard}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
<Card shadow={3} mt={5} mb={5}>
|
||||
<Heading margin={5}>{translate('songsToGetBetter')}</Heading>
|
||||
{!songSuggestions.data ? (
|
||||
<LoadingComponent />
|
||||
) : (
|
||||
<CardGridCustom
|
||||
content={songSuggestions.data.map(swaToSongCardProps)}
|
||||
cardComponent={SongCard}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
type SongsSearchComponentProps = {
|
||||
maxRows?: number;
|
||||
};
|
||||
|
||||
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
||||
const navigation = useNavigation();
|
||||
const { songData } = React.useContext(SearchContext);
|
||||
const favoritesQuery = useQuery(API.getLikedSongs(['artist']));
|
||||
const { mutate } = useLikeSongMutation();
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
<Box>
|
||||
{songData?.length ? (
|
||||
songData.slice(0, props.maxRows).map((comp, index) => (
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
isLiked={
|
||||
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
|
||||
}
|
||||
handleLike={async (state: boolean, songId: number) =>
|
||||
mutate({ songId: songId, like: state })
|
||||
}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, 'song');
|
||||
navigation.navigate('Play', { songId: comp.id });
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Translate translationKey="errNoResults" />
|
||||
)}
|
||||
</Box>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
type ItemSearchComponentProps = {
|
||||
maxItems?: number;
|
||||
};
|
||||
|
||||
const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
const { artistData } = React.useContext(SearchContext);
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Translate translationKey="artistFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
{artistData?.length ? (
|
||||
<CardGridCustom
|
||||
content={artistData
|
||||
.slice(0, props.maxItems ?? artistData.length)
|
||||
.map((artistData) => ({
|
||||
image: API.getArtistIllustration(artistData.id),
|
||||
name: artistData.name,
|
||||
id: artistData.id,
|
||||
onPress: () => {
|
||||
API.createSearchHistoryEntry(artistData.name, 'artist');
|
||||
navigation.navigate('Artist', { artistId: artistData.id });
|
||||
},
|
||||
}))}
|
||||
cardComponent={ArtistCard}
|
||||
/>
|
||||
) : (
|
||||
<Translate translationKey="errNoResults" />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
const { genreData } = React.useContext(SearchContext);
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Translate translationKey="genreFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
{genreData?.length ? (
|
||||
<CardGridCustom
|
||||
content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
|
||||
image: API.getGenreIllustration(g.id),
|
||||
name: g.name,
|
||||
id: g.id,
|
||||
onPress: () => {
|
||||
API.createSearchHistoryEntry(g.name, 'genre');
|
||||
navigation.navigate('Genre', { genreId: g.id });
|
||||
},
|
||||
}))}
|
||||
cardComponent={GenreCard}
|
||||
/>
|
||||
) : (
|
||||
<Translate translationKey="errNoResults" />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const FavoritesComponent = () => {
|
||||
const navigation = useNavigation();
|
||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
||||
|
||||
if (favoritesQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
return <></>;
|
||||
}
|
||||
if (!favoritesQuery.data) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
<Box>
|
||||
{favoritesQuery.data?.map((songData) => (
|
||||
<FavSongRow
|
||||
key={songData.id}
|
||||
song={songData.song}
|
||||
addedDate={songData.addedDate}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(songData.song.name, 'song'); //todo
|
||||
navigation.navigate('Play', { songId: songData.song!.id });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AllComponent = () => {
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isMobileView = screenSize == 'small';
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Flex
|
||||
flexWrap="wrap"
|
||||
direction={isMobileView ? 'column' : 'row'}
|
||||
justifyContent={['flex-start']}
|
||||
mt={4}
|
||||
>
|
||||
<Column w={isMobileView ? '100%' : '50%'}>
|
||||
<Box minH={isMobileView ? 100 : 200}>
|
||||
<ArtistSearchComponent maxItems={6} />
|
||||
</Box>
|
||||
<Box minH={isMobileView ? 100 : 200}>
|
||||
<GenreSearchComponent maxItems={6} />
|
||||
</Box>
|
||||
</Column>
|
||||
<Box w={isMobileView ? '100%' : '50%'}>
|
||||
<SongsSearchComponent maxRows={9} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterSwitch = () => {
|
||||
const { filter } = React.useContext(SearchContext);
|
||||
const [currentFilter, setCurrentFilter] = React.useState(filter);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCurrentFilter(filter);
|
||||
}, [filter]);
|
||||
|
||||
switch (currentFilter) {
|
||||
case 'all':
|
||||
return <AllComponent />;
|
||||
case 'song':
|
||||
return <SongsSearchComponent />;
|
||||
case 'artist':
|
||||
return <ArtistSearchComponent />;
|
||||
case 'genre':
|
||||
return <GenreSearchComponent />;
|
||||
case 'favorites':
|
||||
return <FavoritesComponent />;
|
||||
default:
|
||||
return (
|
||||
<Translate translationKey="unknownError" format={(e) => `${e}: ${currentFilter}`} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const SearchResultComponent = () => {
|
||||
const { stringQuery } = React.useContext(SearchContext);
|
||||
const { filter } = React.useContext(SearchContext);
|
||||
const shouldOutput = !!stringQuery.trim() || filter == 'favorites';
|
||||
|
||||
return shouldOutput ? (
|
||||
<Box p={5}>
|
||||
<FilterSwitch />
|
||||
</Box>
|
||||
) : (
|
||||
<HomeSearchComponent />
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,18 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useMemo, memo } from 'react';
|
||||
import { StyleSheet, ViewStyle, Image } from 'react-native';
|
||||
import { Column, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
|
||||
import { StyleSheet, ViewStyle, Image, Platform } from 'react-native';
|
||||
import {
|
||||
Column,
|
||||
HStack,
|
||||
Row,
|
||||
Stack,
|
||||
Text,
|
||||
useBreakpointValue,
|
||||
useTheme,
|
||||
IconButton,
|
||||
} from 'native-base';
|
||||
import { HeartAdd, HeartRemove, Play } from 'iconsax-react-native';
|
||||
import IconButton from './IconButton';
|
||||
import WebIconButton from './IconButton';
|
||||
import Spacer from '../../components/UI/Spacer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -103,18 +112,15 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
backgroundColor: colors.coolGray[500],
|
||||
paddingRight: screenSize === 'small' ? 8 : 16,
|
||||
},
|
||||
playButtonContainer: {
|
||||
playButton: {
|
||||
backgroundColor: colors.primary[300],
|
||||
borderRadius: 999,
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
right: -8,
|
||||
bottom: -6,
|
||||
},
|
||||
playButton: {
|
||||
backgroundColor: colors.primary[300],
|
||||
borderRadius: 999,
|
||||
},
|
||||
image: {
|
||||
position: 'relative',
|
||||
width: screenSize === 'xl' ? 80 : 60,
|
||||
height: screenSize === 'xl' ? 80 : 60,
|
||||
},
|
||||
@@ -124,6 +130,10 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
},
|
||||
songContainer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
},
|
||||
stats: {
|
||||
display: 'flex',
|
||||
@@ -144,26 +154,30 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
return (
|
||||
<HStack space={screenSize === 'xl' ? 2 : 1} style={[styles.container, props.style]}>
|
||||
<Stack style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<IconButton
|
||||
containerStyle={styles.playButtonContainer}
|
||||
style={styles.playButton}
|
||||
padding={8}
|
||||
onPress={props.onPlay}
|
||||
color="#FFF"
|
||||
icon={Play}
|
||||
variant="Bold"
|
||||
size={24}
|
||||
/>
|
||||
<Image source={{ uri: props.image }} style={styles.image} />
|
||||
<IconButton
|
||||
style={styles.playButton}
|
||||
onPress={props.onPlay}
|
||||
icon={<Play size={24} variant="Bold" color="#FFF" />}
|
||||
/>
|
||||
</Stack>
|
||||
<Column style={{ flex: 4, width: '100%', justifyContent: 'center' }}>
|
||||
<Text numberOfLines={1} style={styles.artistText}>
|
||||
<Text isTruncated numberOfLines={1} style={styles.artistText}>
|
||||
{props.artist}
|
||||
</Text>
|
||||
{screenSize === 'xl' && <Spacer height="xs" />}
|
||||
<Row space={2} style={styles.songContainer}>
|
||||
<Text numberOfLines={1}>{props.song}</Text>
|
||||
<IconButton
|
||||
<Row style={styles.songContainer}>
|
||||
<Text
|
||||
isTruncated
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
{props.song}
|
||||
</Text>
|
||||
{Platform.OS === 'web' ? (
|
||||
<WebIconButton
|
||||
colorActive={colors.text[700]}
|
||||
color={colors.primary[300]}
|
||||
icon={HeartAdd}
|
||||
@@ -173,6 +187,28 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
isActive={props.liked}
|
||||
onPress={props.onLike}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
variant={'unstyled'}
|
||||
icon={
|
||||
props.liked ? (
|
||||
<HeartRemove
|
||||
size={screenSize === 'xl' ? 24 : 18}
|
||||
color={colors.primary[700]}
|
||||
variant="Bold"
|
||||
/>
|
||||
) : (
|
||||
<HeartAdd
|
||||
size={screenSize === 'xl' ? 24 : 18}
|
||||
color={colors.primary[300]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onPress={() => {
|
||||
props.onLike(!props.liked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</Column>
|
||||
{[formattedLastScore, formattedBestScore].map((value, index) => (
|
||||
|
||||
@@ -132,7 +132,9 @@ function MusicListCC({
|
||||
style={{ marginBottom: 2 }}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
keyExtractor={(item) => {
|
||||
return `${item.id}`;
|
||||
}}
|
||||
ListFooterComponent={
|
||||
hasMore ? (
|
||||
<View style={styles.footerContainer}>
|
||||
@@ -157,7 +159,7 @@ function MusicListCC({
|
||||
// Styles for the MusicList component
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
gap: 2,
|
||||
borderRadius: 10,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FunctionComponent } from 'react';
|
||||
import { Linking, Platform, useWindowDimensions } from 'react-native';
|
||||
import ButtonBase from './ButtonBase';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import API, { APIError } from '../../API';
|
||||
import SeparatorBase from './SeparatorBase';
|
||||
import LinkBase from './LinkBase';
|
||||
@@ -38,6 +39,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
const dispatch = useDispatch();
|
||||
const toast = useToast();
|
||||
const colorScheme = useColorScheme();
|
||||
const { t } = useTranslation();
|
||||
const [logo] = useAssets(
|
||||
colorScheme == 'light'
|
||||
? require('../../assets/icon_light.png')
|
||||
@@ -84,7 +86,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
)}
|
||||
</Row>
|
||||
<ButtonBase
|
||||
title={translate('guestMode')}
|
||||
title={t('guestMode')}
|
||||
onPress={async () => {
|
||||
try {
|
||||
handleGuestLogin((accessToken: string) => {
|
||||
@@ -152,7 +154,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
title={translate('continuewithgoogle')}
|
||||
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
|
||||
/>
|
||||
<SeparatorBase>or</SeparatorBase>
|
||||
<SeparatorBase>{t('authOrSection')}</SeparatorBase>
|
||||
<Stack
|
||||
space={3}
|
||||
justifyContent="center"
|
||||
|
||||
@@ -9,7 +9,7 @@ interface TextFormFieldProps extends TextFieldBaseProps {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const ERROR_HEIGHT = 20;
|
||||
const ERROR_HEIGHT = 25;
|
||||
const ERROR_PADDING_TOP = 8;
|
||||
|
||||
const TextFormField: React.FC<TextFormFieldProps> = ({ error, style, ...textFieldBaseProps }) => {
|
||||
@@ -49,7 +49,12 @@ const TextFormField: React.FC<TextFormFieldProps> = ({ error, style, ...textFiel
|
||||
}}
|
||||
>
|
||||
<Warning2 size="16" color="#f7253d" variant="Bold" />
|
||||
<Text isTruncated maxW={'100%'} style={styles.errorText}>
|
||||
<Text
|
||||
isTruncated
|
||||
maxW={'100%'}
|
||||
fontSize={styles.errorText.fontSize}
|
||||
color={styles.errorText.color}
|
||||
>
|
||||
{error}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
@@ -62,14 +67,14 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
},
|
||||
errorContainer: {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 12,
|
||||
},
|
||||
errorText: {
|
||||
color: '#f7253d',
|
||||
fontSize: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import ButtonBase from '../UI/ButtonBase';
|
||||
import { AddSquare, CloseCircle, SearchNormal1 } from 'iconsax-react-native';
|
||||
import { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import Genre from '../../models/Genre';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { searchProps } from '../../views/V2/SearchView';
|
||||
|
||||
type ArtistChipProps = {
|
||||
name: string;
|
||||
@@ -29,9 +29,9 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
||||
}}
|
||||
>
|
||||
{props.selected ? (
|
||||
<CloseCircle size="32" color={'#ED4A51'} />
|
||||
<CloseCircle size="24" color={'#ED4A51'} />
|
||||
) : (
|
||||
<AddSquare size="32" color={'#6075F9'} />
|
||||
<AddSquare size="24" color={'#6075F9'} />
|
||||
)}
|
||||
<Text>{props.name}</Text>
|
||||
</View>
|
||||
@@ -40,15 +40,24 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SearchBarComponent = () => {
|
||||
const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => void }) => {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [genre, setGenre] = React.useState({} as Genre | undefined);
|
||||
const [genre, setGenre] = React.useState('');
|
||||
const [artist, setArtist] = React.useState('');
|
||||
const artistsQuery = useQuery(API.getAllArtists());
|
||||
const genresQuery = useQuery(API.getAllGenres());
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isMobileView = screenSize == 'small';
|
||||
|
||||
const handleValidate = () => {
|
||||
const searchData = {
|
||||
query: query,
|
||||
artist: artistsQuery.data?.find((a) => a.name === artist)?.id ?? undefined,
|
||||
genre: genresQuery.data?.find((g) => g.name === genre)?.id ?? undefined,
|
||||
};
|
||||
props.onValidate(searchData);
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
@@ -57,62 +66,48 @@ const SearchBarComponent = () => {
|
||||
borderBottomColor: '#9E9E9E',
|
||||
display: 'flex',
|
||||
flexDirection: isMobileView ? 'column' : 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
margin: 5,
|
||||
padding: 16,
|
||||
padding: isMobileView ? 8 : 16,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{!!artist && (
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexWrap: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{artist && (
|
||||
<ArtistChipComponent
|
||||
onPress={() => setArtist('')}
|
||||
name={artist}
|
||||
selected={true}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexGrow: 1 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
variant={'unstyled'}
|
||||
placeholder={translate('searchBarPlaceholder')}
|
||||
style={{ width: '100%', height: 30 }}
|
||||
style={{ height: 30, flex: 1 }}
|
||||
onChangeText={(value) => setQuery(value)}
|
||||
/>
|
||||
</View>
|
||||
<ButtonBase
|
||||
type="menu"
|
||||
icon={SearchNormal1}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
/>
|
||||
<ButtonBase type="menu" icon={SearchNormal1} onPress={handleValidate} />
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
@@ -146,6 +141,13 @@ const SearchBarComponent = () => {
|
||||
key={index}
|
||||
name={artist.name}
|
||||
onPress={() => {
|
||||
props.onValidate({
|
||||
artist: artist.id,
|
||||
genre:
|
||||
genresQuery.data?.find((a) => a.name === genre)
|
||||
?.id ?? undefined,
|
||||
query: query,
|
||||
});
|
||||
setArtist(artist.name);
|
||||
}}
|
||||
/>
|
||||
@@ -154,15 +156,20 @@ const SearchBarComponent = () => {
|
||||
</ScrollView>
|
||||
<View>
|
||||
<Select
|
||||
selectedValue={genre?.name}
|
||||
selectedValue={genre}
|
||||
placeholder={translate('genreFilter')}
|
||||
accessibilityLabel="Genre"
|
||||
onValueChange={(itemValue) => {
|
||||
setGenre(
|
||||
genresQuery.data?.find((genre) => {
|
||||
genre.name == itemValue;
|
||||
})
|
||||
);
|
||||
setGenre(itemValue);
|
||||
props.onValidate({
|
||||
artist:
|
||||
artistsQuery.data?.find((a) => a.name === artist)?.id ??
|
||||
undefined,
|
||||
genre:
|
||||
genresQuery.data?.find((g) => g.name === itemValue)?.id ??
|
||||
undefined,
|
||||
query: query,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Select.Item label={translate('emptySelection')} value="" />
|
||||
|
||||
@@ -52,12 +52,10 @@ export const BoardRow = ({ userAvatarUrl, userPseudo, userLvl, index }: BoardRow
|
||||
</View>
|
||||
|
||||
<Text
|
||||
fontSize={16}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontStyle: 'normal',
|
||||
flex: 1,
|
||||
marginHorizontal: 10,
|
||||
fontWeight: '500',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
isTruncated
|
||||
@@ -86,14 +84,7 @@ export const BoardRow = ({ userAvatarUrl, userPseudo, userLvl, index }: BoardRow
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Text fontSize={16} textAlign={'center'}>
|
||||
{index}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -62,15 +62,7 @@ export const PodiumCard = ({
|
||||
</Avatar>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: '500',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
numberOfLines={2}
|
||||
isTruncated
|
||||
>
|
||||
<Text fontSize={20} numberOfLines={2} isTruncated>
|
||||
{userPseudo ?? '---'}
|
||||
</Text>
|
||||
<View
|
||||
@@ -82,12 +74,7 @@ export const PodiumCard = ({
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
<Text fontSize={24} fontWeight={'bold'}>
|
||||
{userLvl ?? '-'}
|
||||
</Text>
|
||||
<MedalStar size="24" variant="Bold" color={medalColor} />
|
||||
|
||||
@@ -307,7 +307,7 @@ export const en = {
|
||||
leaderBoardHeading: 'These are the best players',
|
||||
leaderBoardHeadingFull:
|
||||
'The players having the best scores, thanks to their exceptional accuracy, are highlighted here.',
|
||||
emptySelection: 'None,',
|
||||
emptySelection: 'None',
|
||||
gamesPlayed: 'Games Played',
|
||||
metronome: 'Metronome',
|
||||
loading: 'Loading... Please Wait',
|
||||
@@ -321,6 +321,8 @@ export const en = {
|
||||
accountCreatedOn: 'Account Created on',
|
||||
downloadAPKInstructions:
|
||||
"Go to the latest release, unfold the 'Assets' section, and click 'android-build.apk'.",
|
||||
discoverySuggestionSectionTitle: 'Suggested for you',
|
||||
authOrSection: 'or',
|
||||
};
|
||||
|
||||
export const fr: typeof en = {
|
||||
@@ -643,10 +645,12 @@ export const fr: typeof en = {
|
||||
whatIsChromacase: "Chromacase c'est quoi?",
|
||||
clickHereForMoreInfo: "Cliquez ici pour plus d'info",
|
||||
forgotPassword: "J'ai oublié mon mot de passe",
|
||||
updateProfile: 'Changer le Profile',
|
||||
updateProfile: 'Changer le Profil',
|
||||
accountCreatedOn: 'Compte créé le',
|
||||
downloadAPKInstructions:
|
||||
"Télécharger 'android-build.apk' dans la section 'Assets' de la dernière release",
|
||||
discoverySuggestionSectionTitle: 'Suggéré pour vous',
|
||||
authOrSection: 'ou',
|
||||
};
|
||||
|
||||
export const sp: typeof en = {
|
||||
@@ -980,4 +984,6 @@ export const sp: typeof en = {
|
||||
accountCreatedOn: 'Cuenta creada el',
|
||||
downloadAPKInstructions:
|
||||
"Descargue 'android-build.apk' en la sección 'Assets' de la última versión",
|
||||
discoverySuggestionSectionTitle: 'Sugerido para ti',
|
||||
authOrSection: 'o',
|
||||
};
|
||||
|
||||
@@ -18,11 +18,7 @@ const ForgotPasswordView = () => {
|
||||
return 'Error with email, please contact support';
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<ForgotPasswordForm onSubmit={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
return <ForgotPasswordForm onSubmit={handleSubmit} />;
|
||||
};
|
||||
|
||||
export default ForgotPasswordView;
|
||||
|
||||
@@ -29,22 +29,21 @@ const Leaderboardiew = () => {
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<Text
|
||||
<ScrollView
|
||||
style={{
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fontSize={20}
|
||||
fontWeight={'500'}
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: '500',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{translate('leaderBoardHeading')}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
<Text fontSize={14} fontWeight={'500'}>
|
||||
{translate('leaderBoardHeadingFull')}
|
||||
</Text>
|
||||
<View
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useStopwatch } from 'react-use-precision-timer';
|
||||
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
|
||||
import * as Linking from 'expo-linking';
|
||||
import url from 'url';
|
||||
import PartitionMagic from '../components/Play/PartitionMagic';
|
||||
import PartitionMagic, { updateCursor } from '../components/Play/PartitionMagic';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useTheme, useBreakpointValue } from 'native-base';
|
||||
@@ -25,6 +25,7 @@ import { Clock, Cup } from 'iconsax-react-native';
|
||||
import PlayViewControlBar from '../components/Play/PlayViewControlBar';
|
||||
import ScoreModal from '../components/ScoreModal';
|
||||
import { PlayScore, ScoreMessage } from '../components/Play/PlayScore';
|
||||
import { updateTime as updateTimeControlBar } from '../components/Play/PlayTimestampShow';
|
||||
|
||||
type PlayViewProps = {
|
||||
songId: number;
|
||||
@@ -32,6 +33,7 @@ type PlayViewProps = {
|
||||
|
||||
// this a hot fix this should be reverted soon
|
||||
let scoroBaseApiUrl = process.env.EXPO_PUBLIC_SCORO_URL!;
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') {
|
||||
Linking.getInitialURL().then((initUrl) => {
|
||||
@@ -67,10 +69,12 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
const webSocket = useRef<WebSocket>();
|
||||
const [paused, setPause] = useState<boolean>(true);
|
||||
const stopwatch = useStopwatch();
|
||||
const [time, setTime] = useState(0);
|
||||
// const [time, setTime] = useState(0);
|
||||
const time = useRef(0);
|
||||
const [endResult, setEndResult] = useState<unknown>();
|
||||
const [shouldPlay, setShouldPlay] = useState(false);
|
||||
const songHistory = useQuery(API.getSongHistory(songId));
|
||||
const endCalled = useRef(false);
|
||||
const [score, setScore] = useState(0); // Between 0 and 100
|
||||
const getElapsedTime = () => stopwatch.getElapsedRunningTime();
|
||||
const [readyToPlay, setReadyToPlay] = useState(false);
|
||||
@@ -112,6 +116,8 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
};
|
||||
|
||||
const onEnd = () => {
|
||||
if (endCalled.current == true) return;
|
||||
endCalled.current = true;
|
||||
stopwatch.stop();
|
||||
if (webSocket.current?.readyState != WebSocket.OPEN) {
|
||||
console.warn('onEnd: Websocket not open');
|
||||
@@ -146,8 +152,16 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
bearer: accessToken,
|
||||
})
|
||||
);
|
||||
interval = setInterval(() => {
|
||||
webSocket.current!.send(
|
||||
JSON.stringify({
|
||||
type: 'ping',
|
||||
})
|
||||
);
|
||||
}, 15000);
|
||||
};
|
||||
webSocket.current.onclose = () => {
|
||||
clearInterval(interval);
|
||||
console.log('Websocket closed', endMsgReceived);
|
||||
if (!endMsgReceived) {
|
||||
toast.show({ description: 'Connection lost with Scorometer' });
|
||||
@@ -165,19 +179,30 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
toast.show({ description: 'Scoro: ' + data.error });
|
||||
return;
|
||||
}
|
||||
if (data.type == 'pong') return;
|
||||
if (data.type == 'end') {
|
||||
const maxPoints = data.score.max_score || 1;
|
||||
const points = data.overallScore;
|
||||
|
||||
endMsgReceived = true;
|
||||
webSocket.current?.close();
|
||||
setScore(Math.floor((Math.max(points, 0) * 100) / maxPoints));
|
||||
setEndResult({ songId: song.data!.id, ...data });
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
|
||||
const points = data.info.score;
|
||||
const maxPoints = data.info.max_score || 1;
|
||||
|
||||
setScore(Math.floor((Math.max(points, 0) * 100) / maxPoints));
|
||||
|
||||
if (data.type == 'step') {
|
||||
// setTime(data.timestamp);
|
||||
time.current = data.timestamp;
|
||||
updateCursor?.();
|
||||
updateTimeControlBar?.();
|
||||
return;
|
||||
}
|
||||
let formattedMessage = '';
|
||||
let messageColor: ColorSchemeType | undefined;
|
||||
|
||||
@@ -218,8 +243,9 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
};
|
||||
inputs.forEach((input) => {
|
||||
input.onmidimessage = (message) => {
|
||||
const { command, note } = parseMidiMessage(message);
|
||||
const keyIsPressed = command == 9;
|
||||
const { command, note, velocity } = parseMidiMessage(message);
|
||||
let keyIsPressed = command == 9;
|
||||
if (velocity == 0) keyIsPressed = false;
|
||||
|
||||
webSocket.current?.send(
|
||||
JSON.stringify({
|
||||
@@ -237,15 +263,22 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
||||
if (playType == 'practice') return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTime(() => getElapsedTime()); // Countdown
|
||||
time.current = getElapsedTime();
|
||||
updateCursor?.();
|
||||
updateTimeControlBar?.();
|
||||
}, 200);
|
||||
return () => clearInterval(interval);
|
||||
}, [playType]);
|
||||
|
||||
useEffect(() => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
||||
|
||||
return () => {
|
||||
ScreenOrientation.unlockAsync().catch(() => {});
|
||||
stopwatch.stop();
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -316,7 +349,10 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
style={{}}
|
||||
type="outlined"
|
||||
title={translate('practiceBtn')}
|
||||
onPress={async () => setPlayType('practice')}
|
||||
onPress={async () => {
|
||||
setPlayType('practice');
|
||||
setShouldPlay(true);
|
||||
}}
|
||||
/>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
@@ -386,6 +422,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
}}
|
||||
>
|
||||
<PartitionMagic
|
||||
playType={playType}
|
||||
shouldPlay={shouldPlay}
|
||||
timestamp={time}
|
||||
songID={song.data.id}
|
||||
@@ -406,6 +443,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
/>
|
||||
</View>
|
||||
<PlayViewControlBar
|
||||
playType={playType}
|
||||
score={score}
|
||||
time={time}
|
||||
paused={paused}
|
||||
@@ -416,7 +454,9 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
setShouldPlay(false);
|
||||
}}
|
||||
onResume={() => {
|
||||
setTimeout(() => {
|
||||
setShouldPlay(true);
|
||||
}, 3000);
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -34,7 +34,7 @@ const ProfileView = () => {
|
||||
const isBigScreen = layout.width > 650;
|
||||
|
||||
return (
|
||||
<Flex flex={1}>
|
||||
<Flex flex={1} padding={8}>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -68,9 +68,10 @@ const ProfileView = () => {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<Text fontSize={'xl'} style={{ paddingRight: 'auto' }}>
|
||||
<Text fontSize={'xl'} isTruncated numberOfLines={2} flexShrink={1}>
|
||||
{userQuery.data.name}
|
||||
</Text>
|
||||
<ButtonBase
|
||||
@@ -80,10 +81,15 @@ const ProfileView = () => {
|
||||
/>
|
||||
</View>
|
||||
<Translate
|
||||
style={{ paddingBottom: 10, fontWeight: 'bold' }}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
translationKey="accountCreatedOn"
|
||||
format={(e) => `${e} ${userQuery.data.data.createdAt.toLocaleDateString()}`}
|
||||
/>
|
||||
<Translate
|
||||
style={{ fontWeight: 'bold', paddingBottom: 10 }}
|
||||
translationKey="email"
|
||||
format={(e) => `${e} ${userQuery.data.email}`}
|
||||
/>
|
||||
<Flex
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import Artist from '../models/Artist';
|
||||
import Song from '../models/Song';
|
||||
import Genre from '../models/Genre';
|
||||
import API from '../API';
|
||||
import { useQuery } from '../Queries';
|
||||
import { SearchResultComponent } from '../components/SearchResult';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { Filter } from '../components/SearchBar';
|
||||
import { ScrollView } from 'native-base';
|
||||
import LikedSong from '../models/LikedSong';
|
||||
|
||||
interface SearchContextType {
|
||||
filter: 'artist' | 'song' | 'genre' | 'all' | 'favorites';
|
||||
updateFilter: (newData: 'artist' | 'song' | 'genre' | 'all' | 'favorites') => void;
|
||||
stringQuery: string;
|
||||
updateStringQuery: (newData: string) => void;
|
||||
songData: Song[];
|
||||
artistData: Artist[];
|
||||
genreData: Genre[];
|
||||
favoriteData: LikedSong[];
|
||||
isLoadingSong: boolean;
|
||||
isLoadingArtist: boolean;
|
||||
isLoadingGenre: boolean;
|
||||
isLoadingFavorite: boolean;
|
||||
}
|
||||
|
||||
export const SearchContext = React.createContext<SearchContextType>({
|
||||
filter: 'all',
|
||||
updateFilter: () => {},
|
||||
stringQuery: '',
|
||||
updateStringQuery: () => {},
|
||||
songData: [],
|
||||
artistData: [],
|
||||
genreData: [],
|
||||
favoriteData: [],
|
||||
isLoadingSong: false,
|
||||
isLoadingArtist: false,
|
||||
isLoadingGenre: false,
|
||||
isLoadingFavorite: false,
|
||||
});
|
||||
|
||||
type SearchViewProps = {
|
||||
query?: string;
|
||||
};
|
||||
|
||||
const SearchView = (props: SearchViewProps) => {
|
||||
const [filter, setFilter] = useState<Filter>('all');
|
||||
const [stringQuery, setStringQuery] = useState<string>(props?.query ?? '');
|
||||
|
||||
const { isLoading: isLoadingSong, data: songData = [] } = useQuery(
|
||||
API.searchSongs(stringQuery),
|
||||
{ enabled: !!stringQuery }
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingArtist, data: artistData = [] } = useQuery(
|
||||
API.searchArtists(stringQuery),
|
||||
{ enabled: !!stringQuery }
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingGenre, data: genreData = [] } = useQuery(
|
||||
API.searchGenres(stringQuery),
|
||||
{ enabled: !!stringQuery }
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingFavorite, data: favoriteData = [] } = useQuery(
|
||||
API.getLikedSongs(),
|
||||
{ enabled: true }
|
||||
);
|
||||
|
||||
const updateFilter = (newData: Filter) => {
|
||||
// called when the filter is changed
|
||||
setFilter(newData);
|
||||
};
|
||||
|
||||
const updateStringQuery = (newData: string) => {
|
||||
// called when the stringQuery is updated
|
||||
setStringQuery(newData);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<SafeAreaView>
|
||||
<SearchContext.Provider
|
||||
value={{
|
||||
filter,
|
||||
stringQuery,
|
||||
songData,
|
||||
artistData,
|
||||
genreData,
|
||||
favoriteData,
|
||||
isLoadingSong,
|
||||
isLoadingArtist,
|
||||
isLoadingGenre,
|
||||
isLoadingFavorite,
|
||||
updateFilter,
|
||||
updateStringQuery,
|
||||
}}
|
||||
>
|
||||
<SearchBar />
|
||||
<SearchResultComponent />
|
||||
</SearchContext.Provider>
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchView;
|
||||
@@ -6,6 +6,7 @@ import SongCardInfo from '../../components/V2/SongCardInfo';
|
||||
import API from '../../API';
|
||||
import { useNavigation } from '../../Navigation';
|
||||
import GoldenRatio from '../../components/V2/GoldenRatio';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
|
||||
const HomeView = () => {
|
||||
const suggestionsQuery = useQuery(
|
||||
@@ -16,7 +17,6 @@ const HomeView = () => {
|
||||
const isPhone = screenSize === 'small';
|
||||
const topSuggestions = suggestionsQuery.data?.slice(0, 4) ?? [];
|
||||
const suggestions = suggestionsQuery.data?.slice(4) ?? [];
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View
|
||||
@@ -25,6 +25,7 @@ const HomeView = () => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -46,15 +47,15 @@ const HomeView = () => {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fontSize={24}
|
||||
fontWeight={'bold'}
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 16,
|
||||
marginBottom: 16,
|
||||
marginTop: 24,
|
||||
}}
|
||||
>
|
||||
{'Suggestions'}
|
||||
{translate('discoverySuggestionSectionTitle')}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -1,12 +1,48 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useQuery } from '../../Queries';
|
||||
import SearchBarComponent from '../../components/V2/SearchBar';
|
||||
import SearchHistory from '../../components/V2/SearchHistory';
|
||||
import { View } from 'react-native';
|
||||
import API from '../../API';
|
||||
import LoadingComponent from '../../components/Loading';
|
||||
import MusicListCC from '../../components/UI/MusicList';
|
||||
|
||||
export type searchProps = {
|
||||
artist: number | undefined;
|
||||
genre: number | undefined;
|
||||
query: string;
|
||||
};
|
||||
|
||||
const SearchView = () => {
|
||||
const artistsQuery = useQuery(API.getAllArtists());
|
||||
const [searchQuery, setSearchQuery] = React.useState({} as searchProps);
|
||||
const rawResult = useQuery(API.searchSongs(searchQuery, ['artist']), {
|
||||
enabled: !!searchQuery.query || !!searchQuery.artist || !!searchQuery.genre,
|
||||
onSuccess() {
|
||||
const artist =
|
||||
artistsQuery?.data?.find(({ id }) => id == searchQuery.artist)?.name ??
|
||||
'unknown artist';
|
||||
searchQuery.query ? API.createSearchHistoryEntry(searchQuery.query, 'song') : null;
|
||||
if (artist != 'unknown artist') API.createSearchHistoryEntry(artist, 'artist');
|
||||
},
|
||||
});
|
||||
|
||||
if (artistsQuery.isLoading) {
|
||||
return <LoadingComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ display: 'flex', gap: 50 }}>
|
||||
<SearchBarComponent />
|
||||
<View style={{ display: 'flex', gap: 20, padding: 8 }}>
|
||||
<SearchBarComponent onValidate={(query) => setSearchQuery(query)} />
|
||||
{rawResult.isSuccess ? (
|
||||
<MusicListCC
|
||||
musics={rawResult.data}
|
||||
isFetching={rawResult.isFetching}
|
||||
refetch={rawResult.refetch}
|
||||
/>
|
||||
) : (
|
||||
<SearchHistory />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ class Key:
|
||||
self.start = start
|
||||
self.duration = duration
|
||||
self.done = False
|
||||
self.half_done = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.key} ({self.start} - {self.duration})"
|
||||
|
||||
@@ -17,6 +17,11 @@ class StartMessage(ValidatedDC):
|
||||
mode: Literal["normal", "practice"]
|
||||
type: Literal["start"] = "start"
|
||||
|
||||
@dataclass
|
||||
class PingMessage(ValidatedDC):
|
||||
type: Literal["ping"] = "ping"
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class EndMessage(ValidatedDC):
|
||||
@@ -52,6 +57,7 @@ message_map = {
|
||||
"note_on": NoteOnMessage,
|
||||
"note_off": NoteOffMessage,
|
||||
"pause": PauseMessage,
|
||||
"ping": PingMessage,
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +68,8 @@ def getMessage() -> (
|
||||
| NoteOnMessage
|
||||
| NoteOffMessage
|
||||
| PauseMessage
|
||||
| InvalidMessage,
|
||||
| InvalidMessage
|
||||
| PingMessage,
|
||||
str,
|
||||
]
|
||||
):
|
||||
|
||||
@@ -9,8 +9,9 @@ from mido import MidiFile
|
||||
|
||||
RATIO = 1.0
|
||||
|
||||
def getPartition(midiFile: str) -> Partition:
|
||||
notes = []
|
||||
def getPartition(midiFile: str, cursors) -> Partition:
|
||||
notes = [Key(i["note"], cursor["timestamp"], i["duration"]) for cursor in cursors for i in cursor["notes"]]
|
||||
'''
|
||||
s = 0
|
||||
notes_on = {}
|
||||
prev_note_on = {}
|
||||
@@ -29,6 +30,7 @@ def getPartition(midiFile: str) -> Partition:
|
||||
note_start = notes_on[d["note"]]
|
||||
notes.append(Key(d["note"], note_start, duration - 10))
|
||||
notes_on[d["note"]] = s # 500
|
||||
'''
|
||||
return Partition(midiFile, notes)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from chroma_case.Message import (
|
||||
NoteOffMessage,
|
||||
NoteOnMessage,
|
||||
PauseMessage,
|
||||
PingMessage,
|
||||
StartMessage,
|
||||
getMessage,
|
||||
)
|
||||
@@ -84,15 +85,17 @@ def send(o):
|
||||
|
||||
class Scorometer:
|
||||
def __init__(self, mode: int, midiFile: str, song_id: int, user_id: int) -> None:
|
||||
self.partition: Partition = getPartition(midiFile)
|
||||
r = requests.get(f"{BACK_URL}/song/{song_id}/assets/cursors", headers=auth_header).json()
|
||||
cursors = r["cursors"]
|
||||
self.partition: Partition = getPartition(midiFile, cursors)
|
||||
self.practice_partition: list[list[Key]] = self.getPracticePartition(mode)
|
||||
# the log generated is so long that it's longer than the stderr buffer resulting in a crash
|
||||
# logging.debug({"partition": self.partition.notes})
|
||||
self.keys_down = []
|
||||
self.keys_down: list[Key] = []
|
||||
self.mode: int = mode
|
||||
self.song_id: int = song_id
|
||||
self.user_id: int = user_id
|
||||
self.wrong_ids = []
|
||||
self.wrong_ids = set()
|
||||
self.difficulties = {}
|
||||
self.info: ScoroInfo = {
|
||||
"max_score": len(self.partition.notes) * 100,
|
||||
@@ -106,6 +109,14 @@ class Scorometer:
|
||||
"max_streak": 0,
|
||||
}
|
||||
|
||||
# Practice variables
|
||||
if self.mode == PRACTICE:
|
||||
self.to_play = set([x.key for x in self.practice_partition.pop(0)])
|
||||
else:
|
||||
self.to_play = set()
|
||||
self.keys_down_practice: set = set()
|
||||
|
||||
|
||||
def send(self, obj):
|
||||
obj["info"] = self.info
|
||||
obj["game_id"] = str(game_uuid) if not testing else "test"
|
||||
@@ -154,13 +165,14 @@ class Scorometer:
|
||||
else 50
|
||||
)
|
||||
self.incrementStreak()
|
||||
to_play.half_done = True
|
||||
logging.debug({"note_on": f"{perf} on {message.note}"})
|
||||
self.send({"type": "timing", "id": message.id, "timing": perf})
|
||||
else:
|
||||
self.info["score"] -= 25
|
||||
self.info["wrong"] += 1
|
||||
self.info["current_streak"] = 0
|
||||
self.wrong_ids += [message.id]
|
||||
self.wrong_ids.add(message.id)
|
||||
logging.debug({"note_on": f"wrong key {message.note}"})
|
||||
self.send({"type": "timing", "id": message.id, "timing": "wrong"})
|
||||
|
||||
@@ -171,8 +183,10 @@ class Scorometer:
|
||||
)
|
||||
self.keys_down.remove((message.note, down_since))
|
||||
if message.id in self.wrong_ids:
|
||||
self.wrong_ids.remove(message.id)
|
||||
logging.debug({"note_off": f"wrong key {message.note}"})
|
||||
self.send({"type": "duration", "id": message.id, "duration": "wrong"})
|
||||
|
||||
return
|
||||
key = Key(
|
||||
key=message.note, start=down_since, duration=(message.time - down_since)
|
||||
@@ -195,67 +209,24 @@ class Scorometer:
|
||||
logging.warning("note_off: no key to play but it was not a wrong note_on")
|
||||
|
||||
def handleNoteOnPractice(self, message: NoteOnMessage):
|
||||
is_down = any(x[0] == message.note for x in self.keys_down)
|
||||
logging.debug({"note_on": message.note})
|
||||
if is_down:
|
||||
return
|
||||
self.keys_down.append((message.note, message.time))
|
||||
key = Key(key=message.note, start=message.time, duration=0)
|
||||
keys_to_play = next(
|
||||
(i for i in self.practice_partition if any(x.done is not True for x in i)),
|
||||
None,
|
||||
)
|
||||
if keys_to_play is None:
|
||||
self.send({"type": "error", "error": "no keys should be played"})
|
||||
return
|
||||
to_play = next(
|
||||
(i for i in keys_to_play if i.key == key.key and i.done is not True), None
|
||||
)
|
||||
if to_play:
|
||||
perf = "practice"
|
||||
logging.debug({"note_on": f"{perf} on {message.note}"})
|
||||
self.incrementStreak()
|
||||
self.send({"type": "timing", "id": message.id, "timing": perf})
|
||||
else:
|
||||
self.wrong_ids += [message.id]
|
||||
logging.debug({"note_on": f"wrong key {message.note}"})
|
||||
self.info["current_streak"] = 0
|
||||
self.send({"type": "timing", "id": message.id, "timing": "wrong"})
|
||||
self.keys_down_practice.add(message.note)
|
||||
self.practiceCheck()
|
||||
|
||||
def handleNoteOffPractice(self, message: NoteOffMessage):
|
||||
logging.debug({"note_off": message.note})
|
||||
down_since = next(
|
||||
since for (h_key, since) in self.keys_down if h_key == message.note
|
||||
)
|
||||
self.keys_down.remove((message.note, down_since))
|
||||
if message.id in self.wrong_ids:
|
||||
logging.debug({"note_off": f"wrong key {message.note}"})
|
||||
self.send({"type": "duration", "id": message.id, "duration": "wrong"})
|
||||
self.info["wrong"] += 1;
|
||||
return
|
||||
key = Key(
|
||||
key=message.note, start=down_since, duration=(message.time - down_since)
|
||||
)
|
||||
keys_to_play = next(
|
||||
(i for i in self.practice_partition if any(x.done is not True for x in i)),
|
||||
None,
|
||||
)
|
||||
if keys_to_play is None:
|
||||
logging.info("Invalid key.")
|
||||
self.info["score"] -= 50
|
||||
# TODO: I dont think this if is right
|
||||
# self.sendScore(message.id, "wrong key", "wrong key")
|
||||
return
|
||||
to_play = next(
|
||||
(i for i in keys_to_play if i.key == key.key and i.done is not True), None
|
||||
)
|
||||
if to_play:
|
||||
perf = "practice"
|
||||
to_play.done = True
|
||||
logging.debug({"note_off": f"{perf} on {message.note}"})
|
||||
self.send({"type": "duration", "id": message.id, "duration": perf})
|
||||
else:
|
||||
self.send({"type": "duration", "id": message.id, "duration": "wrong"})
|
||||
self.keys_down_practice.remove(message.note)
|
||||
self.practiceCheck()
|
||||
|
||||
def practiceCheck(self):
|
||||
if self.to_play == self.keys_down_practice:
|
||||
|
||||
self.info["perfect"] += len(self.to_play)
|
||||
self.info["score"] += 100 * len(self.to_play)
|
||||
if len(self.practice_partition) == 0:
|
||||
self.endGamePractice()
|
||||
self.send({"type": "step", "timestamp": self.practice_partition[0][0].start + 1})
|
||||
self.to_play = set([x.key for x in self.practice_partition.pop(0)])
|
||||
|
||||
def getDurationScore(self, key: Key, to_play: Key):
|
||||
tempo_percent = abs((key.duration / to_play.duration) - 1)
|
||||
@@ -287,13 +258,16 @@ class Scorometer:
|
||||
| NoteOnMessage
|
||||
| NoteOffMessage
|
||||
| PauseMessage
|
||||
| InvalidMessage,
|
||||
| InvalidMessage
|
||||
| PingMessage,
|
||||
line: str,
|
||||
):
|
||||
match message:
|
||||
case InvalidMessage(error):
|
||||
logging.warning(f"Invalid message {line} with error: {error}")
|
||||
self.send({"error": f"Invalid message {line} with error: {error}"})
|
||||
case PingMessage():
|
||||
self.send({"type": "pong"})
|
||||
case NoteOnMessage():
|
||||
if self.mode == NORMAL:
|
||||
self.handleNoteOn(message)
|
||||
@@ -321,7 +295,7 @@ class Scorometer:
|
||||
|
||||
def endGame(self):
|
||||
for i in self.partition.notes:
|
||||
if i.done is False:
|
||||
if i.done is False and not i.half_done:
|
||||
self.info["score"] -= 25
|
||||
self.info["missed"] += 1
|
||||
send(
|
||||
@@ -346,6 +320,17 @@ class Scorometer:
|
||||
)
|
||||
exit()
|
||||
|
||||
def endGamePractice(self):
|
||||
send(
|
||||
{
|
||||
"type": "end",
|
||||
"overallScore": self.info["score"],
|
||||
"precision": round(((self.info["perfect"] + self.info["great"] + self.info["good"]) / (len(self.partition.notes) + self.info["wrong"]) * 100), 2),
|
||||
"score": self.info,
|
||||
}
|
||||
)
|
||||
exit()
|
||||
|
||||
|
||||
def handleStartMessage(start_message: StartMessage):
|
||||
mode = PRACTICE if start_message.mode == "practice" else NORMAL
|
||||
|
||||
Reference in New Issue
Block a user