Compare commits
5 Commits
main
...
clem-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae0bbea3a | ||
|
|
f5136ae59b | ||
|
|
f632ed42a3 | ||
|
|
00ee5cd531 | ||
|
|
1d496301d9 |
@@ -1,3 +1,3 @@
|
||||
FROM node:18.10.0
|
||||
FROM node:17
|
||||
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, Put } from "@nestjs/common";
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ScoresService } from "./scores.service";
|
||||
import { User } from "@prisma/client";
|
||||
@@ -13,10 +13,4 @@ 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")
|
||||
@Get("songs/:query")
|
||||
@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", new ParseIntPipe({ optional: true })) artistId: number,
|
||||
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number,
|
||||
@Query("artistId") artistId: number,
|
||||
@Query("genreId") 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")
|
||||
@Get("artists/:query")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
|
||||
@@ -5,6 +5,7 @@ volumes:
|
||||
scoro_logs:
|
||||
meilisearch:
|
||||
|
||||
|
||||
services:
|
||||
back:
|
||||
#platform: linux/amd64
|
||||
@@ -21,7 +22,7 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
meilisearch:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
@@ -92,14 +93,15 @@ services:
|
||||
- "4567:4567"
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.5
|
||||
image: getmeili/meilisearch:v1.4
|
||||
volumes:
|
||||
- meilisearch:/meili_data
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
|
||||
103
front/API.ts
103
front/API.ts
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -23,7 +24,6 @@ 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,6 +497,84 @@ 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
|
||||
@@ -702,29 +780,6 @@ 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,8 +30,9 @@ const phoneLightGlassmorphism = {
|
||||
900: 'rgb(248, 250, 254)',
|
||||
1000: 'rgb(252, 254, 254)',
|
||||
};
|
||||
|
||||
const defaultDarkGlassmorphism = {
|
||||
const lightGlassmorphism =
|
||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
||||
const darkGlassmorphism = {
|
||||
50: 'rgba(16,16,20,0.9)',
|
||||
100: 'rgba(16,16,20,0.1)',
|
||||
200: 'rgba(16,16,20,0.2)',
|
||||
@@ -45,24 +46,6 @@ const defaultDarkGlassmorphism = {
|
||||
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,32 +2,23 @@ import * as React from 'react';
|
||||
import { Platform, View } from 'react-native';
|
||||
import API from '../../API';
|
||||
import { useQuery } from '../../Queries';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated';
|
||||
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
|
||||
import { CursorInfoItem } from '../../models/SongCursorInfos';
|
||||
import { Audio } from 'expo-av';
|
||||
import { SvgContainer } from './SvgContainer';
|
||||
import LoadingComponent from '../Loading';
|
||||
import { SplendidGrandPiano } from 'smplr';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { useStopwatch } from 'react-use-precision-timer';
|
||||
|
||||
export const timestampAtom = atom(0);
|
||||
export const shouldPlayAtom = atom(false);
|
||||
export const partitionStateAtom = atom(
|
||||
'loading' as 'loading' | 'ready' | 'playing' | 'paused' | 'ended' | 'error'
|
||||
);
|
||||
|
||||
export type ParitionMagicProps = {
|
||||
playType: 'practice' | 'normal' | null;
|
||||
timestamp: React.MutableRefObject<number>;
|
||||
songID: number;
|
||||
shouldPlay: boolean;
|
||||
onEndReached: () => void;
|
||||
onError: (err: string) => void;
|
||||
onReady: () => void;
|
||||
onPlay: () => void;
|
||||
onPause: () => void;
|
||||
};
|
||||
|
||||
const getSVGURL = (songID: number) => {
|
||||
return API.getPartitionSvgUrl(songID);
|
||||
};
|
||||
|
||||
const getCursorToPlay = (
|
||||
@@ -40,35 +31,41 @@ const getCursorToPlay = (
|
||||
return;
|
||||
}
|
||||
for (let i = cursorInfos.length - 1; i > currentCurIdx; i--) {
|
||||
if (cursorInfos[i]!.timestamp <= timestamp) {
|
||||
return onCursorMove(cursorInfos[i]!, i);
|
||||
const cursorInfo = cursorInfos[i]!;
|
||||
if (cursorInfo.timestamp <= timestamp) {
|
||||
onCursorMove(cursorInfo, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const transitionDuration = 50;
|
||||
const startResumePauseWatch = (stopwatch: ReturnType<typeof useStopwatch>, shouldPlay: boolean) => {
|
||||
if (shouldPlay) {
|
||||
if (stopwatch.isPaused()) {
|
||||
stopwatch.resume();
|
||||
return;
|
||||
}
|
||||
stopwatch.start();
|
||||
return;
|
||||
}
|
||||
stopwatch.pause();
|
||||
};
|
||||
|
||||
export let updateCursor: (() => void) | undefined = undefined;
|
||||
const transitionDuration = 200;
|
||||
|
||||
const PartitionMagic = ({
|
||||
playType,
|
||||
timestamp,
|
||||
songID,
|
||||
shouldPlay,
|
||||
onEndReached,
|
||||
onError,
|
||||
onReady,
|
||||
onPlay,
|
||||
onPause,
|
||||
}: ParitionMagicProps) => {
|
||||
const PartitionMagic = ({ songID }: ParitionMagicProps) => {
|
||||
const { data, isLoading, isError } = useQuery(API.getSongCursorInfos(songID));
|
||||
const currentCurIdx = React.useRef(-1);
|
||||
const stopwatch = useStopwatch();
|
||||
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
|
||||
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
|
||||
const partitionOffset = useSharedValue(0);
|
||||
const melodySound = React.useRef<Audio.Sound | null>(null);
|
||||
const piano = React.useRef<SplendidGrandPiano | null>(null);
|
||||
const [isPianoLoaded, setIsPianoLoaded] = React.useState(false);
|
||||
const [timestamp, setTimestamp] = useAtom(timestampAtom);
|
||||
const [shouldPlay, setShouldPlay] = useAtom(shouldPlayAtom);
|
||||
const [partitionState, setPartitionState] = useAtom(partitionStateAtom);
|
||||
const cursorPaddingVertical = 10;
|
||||
const cursorPaddingHorizontal = 3;
|
||||
|
||||
@@ -80,49 +77,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,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
console.log('state', partitionState, timestamp);
|
||||
|
||||
if (!endPartitionReached && currentCurIdx.current + 1 === data?.cursors.length) {
|
||||
// weird contraption but the mobile don't want classic functions to be called
|
||||
// with the withTiming function :(
|
||||
setEndPartitionReached(true);
|
||||
melodySound.current?.pauseAsync();
|
||||
piano.current?.stop();
|
||||
setPartitionState('ended');
|
||||
}
|
||||
|
||||
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 (isError) {
|
||||
setPartitionState('error');
|
||||
}
|
||||
}, [isError]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isPartitionSvgLoaded && !isLoading && (melodySound.current?._loaded || isPianoLoaded)) {
|
||||
setPartitionState('ready');
|
||||
} else if (partitionState !== 'loading') {
|
||||
setPartitionState('loading');
|
||||
}
|
||||
|
||||
return () => {
|
||||
setPartitionState('loading');
|
||||
setTimestamp(0);
|
||||
setShouldPlay(false);
|
||||
};
|
||||
}, [isPartitionSvgLoaded, isLoading, melodySound.current?._loaded, isPianoLoaded]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (Platform.OS === 'web' && !piano.current) {
|
||||
const audio = new AudioContext();
|
||||
piano.current = new SplendidGrandPiano(audio);
|
||||
@@ -130,14 +115,9 @@ const PartitionMagic = ({
|
||||
setIsPianoLoaded(true);
|
||||
});
|
||||
} else if (!melodySound.current) {
|
||||
Audio.Sound.createAsync(
|
||||
{
|
||||
uri: API.getPartitionMelodyUrl(songID),
|
||||
},
|
||||
{
|
||||
progressUpdateIntervalMillis: 200,
|
||||
}
|
||||
).then((track) => {
|
||||
Audio.Sound.createAsync({
|
||||
uri: API.getPartitionMelodyUrl(songID),
|
||||
}).then((track) => {
|
||||
melodySound.current = track.sound;
|
||||
});
|
||||
}
|
||||
@@ -153,55 +133,69 @@ const PartitionMagic = ({
|
||||
piano.current = null;
|
||||
}
|
||||
};
|
||||
}, [playType]);
|
||||
}, []);
|
||||
const partitionDims = React.useMemo<[number, number]>(() => {
|
||||
return [data?.pageWidth ?? 0, data?.pageHeight ?? 1];
|
||||
}, [data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onError && isError) {
|
||||
onError('Error while loading partition');
|
||||
return;
|
||||
}
|
||||
}, [onError, isError]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isPartitionSvgLoaded && !isLoading && (melodySound.current?._loaded || isPianoLoaded)) {
|
||||
onReady();
|
||||
}
|
||||
}, [isPartitionSvgLoaded, isLoading, melodySound.current?._loaded, isPianoLoaded]);
|
||||
const interval = setInterval(
|
||||
() => {
|
||||
// if (partitionState !== 'playing') return;
|
||||
setTimestamp(stopwatch.getElapsedRunningTime());
|
||||
},
|
||||
Platform.OS === 'web' ? 200 : 500
|
||||
);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (Platform.OS === 'web') {
|
||||
if (!piano.current || !isPianoLoaded) {
|
||||
return;
|
||||
}
|
||||
shouldPlay ? onPlay() : onPause();
|
||||
return;
|
||||
}
|
||||
if (!melodySound.current || !melodySound.current._loaded) {
|
||||
if (!piano.current || !isPianoLoaded) return;
|
||||
setPartitionState(shouldPlay ? 'playing' : 'paused');
|
||||
startResumePauseWatch(stopwatch, shouldPlay);
|
||||
return;
|
||||
}
|
||||
if (!melodySound.current || !melodySound.current._loaded) return;
|
||||
setPartitionState(shouldPlay ? 'playing' : 'paused');
|
||||
startResumePauseWatch(stopwatch, shouldPlay);
|
||||
if (shouldPlay) {
|
||||
melodySound.current.playAsync().then(onPlay).catch(console.error);
|
||||
melodySound.current.playAsync().catch(console.error);
|
||||
} else {
|
||||
melodySound.current.pauseAsync().then(onPause).catch(console.error);
|
||||
melodySound.current.pauseAsync().catch(console.error);
|
||||
}
|
||||
}, [shouldPlay]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (endPartitionReached && playType === 'normal') {
|
||||
// if the audio is unsync
|
||||
melodySound.current?.pauseAsync();
|
||||
onEndReached();
|
||||
}
|
||||
}, [endPartitionReached]);
|
||||
if (!shouldPlay) return;
|
||||
if (!melodySound.current || !melodySound.current._loaded) 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),
|
||||
}
|
||||
);
|
||||
if (!piano.current || !isPianoLoaded) return;
|
||||
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={{
|
||||
@@ -235,20 +229,18 @@ const PartitionMagic = ({
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
aspectRatio: partitionDims[0] / partitionDims[1],
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
aspectRatio: partitionDims[0] / partitionDims[1],
|
||||
left: `${partitionOffset.value * 100}%`,
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<SvgContainer
|
||||
url={getSVGURL(songID)}
|
||||
url={API.getPartitionSvgUrl(songID)}
|
||||
onReady={() => {
|
||||
setIsPartitionSvgLoaded(true);
|
||||
}}
|
||||
@@ -276,11 +268,4 @@ const PartitionMagic = ({
|
||||
);
|
||||
};
|
||||
|
||||
PartitionMagic.defaultProps = {
|
||||
onError: () => {},
|
||||
onReady: () => {},
|
||||
onPlay: () => {},
|
||||
onPause: () => {},
|
||||
};
|
||||
|
||||
export default PartitionMagic;
|
||||
|
||||
31
front/components/Play/PartitionTimestampText.tsx
Normal file
31
front/components/Play/PartitionTimestampText.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Text, useTheme } from 'native-base';
|
||||
import { timestampAtom, partitionStateAtom } from './PartitionMagic';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
export const TimestampDisplay = () => {
|
||||
const [time] = useAtom(timestampAtom);
|
||||
const [partitionState] = useAtom(partitionStateAtom);
|
||||
const { colors } = useTheme();
|
||||
const textColor = colors.text;
|
||||
const paused = partitionState === 'paused';
|
||||
if (time < 0) {
|
||||
if (paused) {
|
||||
return <Text color={textColor[900]}>0:00</Text>;
|
||||
}
|
||||
return (
|
||||
<Text color={textColor[900]}>
|
||||
{Math.floor((time % 60000) / 1000)
|
||||
.toFixed(0)
|
||||
.toString()}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text color={textColor[900]}>
|
||||
{`${Math.floor(time / 60000)}:${Math.floor((time % 60000) / 1000)
|
||||
.toFixed(0)
|
||||
.toString()
|
||||
.padStart(2, '0')}`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
31
front/components/Play/PlayEndModal.tsx
Normal file
31
front/components/Play/PlayEndModal.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import PopupCC from '../UI/PopupCC';
|
||||
import { shouldEndAtom } from './PlayViewControlBar';
|
||||
import { partitionStateAtom } from './PartitionMagic';
|
||||
import ScoreModal from '../ScoreModal';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
export const PlayEndModal = () => {
|
||||
const [shouldEnd] = useAtom(shouldEndAtom);
|
||||
const [partitionState] = useAtom(partitionStateAtom);
|
||||
|
||||
const isEnd = shouldEnd || partitionState === 'ended';
|
||||
return (
|
||||
<PopupCC isVisible={isEnd}>
|
||||
<ScoreModal
|
||||
songId={0}
|
||||
overallScore={0}
|
||||
precision={0}
|
||||
score={{
|
||||
max_score: 0,
|
||||
missed: 0,
|
||||
wrong: 0,
|
||||
good: 0,
|
||||
great: 0,
|
||||
perfect: 0,
|
||||
current_streak: 0,
|
||||
max_streak: 0,
|
||||
}}
|
||||
/>
|
||||
</PopupCC>
|
||||
);
|
||||
};
|
||||
@@ -10,20 +10,21 @@ import Animated, {
|
||||
Easing,
|
||||
} from 'react-native-reanimated';
|
||||
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
|
||||
export const scoreMessageAtom = atom<ScoreMessage | null>(null);
|
||||
export const scoreAtom = atom(0);
|
||||
|
||||
export type ScoreMessage = {
|
||||
content: string;
|
||||
color?: ColorSchemeType;
|
||||
id: number;
|
||||
};
|
||||
|
||||
type PlayScoreProps = {
|
||||
score: number;
|
||||
timestamp: number;
|
||||
streak: number;
|
||||
message?: ScoreMessage;
|
||||
};
|
||||
|
||||
export const PlayScore = ({ score, streak, message }: PlayScoreProps) => {
|
||||
export const PlayScore = () => {
|
||||
const [message] = useAtom(scoreMessageAtom);
|
||||
const [score] = useAtom(scoreAtom);
|
||||
const scoreMessageScale = useSharedValue(0);
|
||||
// this style should bounce in on enter and fade away
|
||||
const scoreMsgStyle = useAnimatedStyle(() => {
|
||||
@@ -92,9 +93,9 @@ export const PlayScore = ({ score, streak, message }: PlayScoreProps) => {
|
||||
<Text color={textColor[900]} fontSize={20}>
|
||||
{message.content}
|
||||
</Text>
|
||||
{streak > 0 && (
|
||||
{message.streak > 0 && (
|
||||
<Text color={textColor[900]} fontSize={15} bold>
|
||||
{`x${streak}`}
|
||||
{`x${message.streak}`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +1,40 @@
|
||||
import { View } from 'react-native';
|
||||
import * as React from 'react';
|
||||
import { Row, Image, Text, useBreakpointValue, IconButton } from 'native-base';
|
||||
import { Row, Image, Text, useBreakpointValue, IconButton, Button } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { MetronomeControls } from '../Metronome';
|
||||
import StarProgress from '../StarProgress';
|
||||
import Song from '../../models/Song';
|
||||
import { useTheme } from 'native-base';
|
||||
import { PlayTimestampShow } from './PlayTimestampShow';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { partitionStateAtom, shouldPlayAtom } from './PartitionMagic';
|
||||
import { TimestampDisplay } from './PartitionTimestampText';
|
||||
|
||||
export const shouldEndAtom = atom(false);
|
||||
|
||||
type PlayViewControlBarProps = {
|
||||
playType: 'practice' | 'normal' | null;
|
||||
song: Song;
|
||||
time: React.MutableRefObject<number>;
|
||||
paused: boolean;
|
||||
score: number;
|
||||
disabled: boolean;
|
||||
onResume: () => void;
|
||||
onPause: () => void;
|
||||
onEnd: () => void;
|
||||
};
|
||||
|
||||
const PlayViewControlBar = ({
|
||||
playType,
|
||||
song,
|
||||
time,
|
||||
paused,
|
||||
score,
|
||||
disabled,
|
||||
onResume,
|
||||
onPause,
|
||||
onEnd,
|
||||
}: PlayViewControlBarProps) => {
|
||||
const PlayViewControlBar = ({ song }: PlayViewControlBarProps) => {
|
||||
const [, setShouldPlay] = useAtom(shouldPlayAtom);
|
||||
const [partitionState] = useAtom(partitionStateAtom);
|
||||
const [, setShouldEnd] = useAtom(shouldEndAtom);
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const bpm = React.useRef<number>(60);
|
||||
const { colors } = useTheme();
|
||||
const textColor = colors.text;
|
||||
const isPlaying = partitionState === 'playing';
|
||||
const isPartitionLoading = partitionState === 'loading';
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
setShouldPlay(false);
|
||||
setShouldEnd(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Row
|
||||
style={{
|
||||
@@ -110,17 +109,18 @@ const PlayViewControlBar = ({
|
||||
gap: isPhone ? 10 : 25,
|
||||
}}
|
||||
>
|
||||
{playType != 'practice' && (
|
||||
{isPartitionLoading ? (
|
||||
<Button isLoading size="sm" variant="solid" color={colors.coolGray[900]} />
|
||||
) : (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="solid"
|
||||
disabled={disabled}
|
||||
_icon={{
|
||||
as: Ionicons,
|
||||
color: colors.coolGray[900],
|
||||
name: paused ? 'play' : 'pause',
|
||||
name: isPlaying ? 'pause' : 'play',
|
||||
}}
|
||||
onPress={paused ? onResume : onPause}
|
||||
onPress={() => setShouldPlay(!isPlaying)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
@@ -131,11 +131,10 @@ const PlayViewControlBar = ({
|
||||
as: Ionicons,
|
||||
name: 'stop',
|
||||
}}
|
||||
onPress={onEnd}
|
||||
onPress={() => setShouldEnd(true)}
|
||||
/>
|
||||
<PlayTimestampShow paused={paused} time={time} />
|
||||
<TimestampDisplay />
|
||||
<StarProgress
|
||||
value={score}
|
||||
max={100}
|
||||
starSteps={[50, 75, 90]}
|
||||
style={{
|
||||
@@ -156,17 +155,10 @@ const PlayViewControlBar = ({
|
||||
minWidth: 120,
|
||||
}}
|
||||
>
|
||||
{playType != 'practice' && <MetronomeControls paused={paused} bpm={bpm.current} />}
|
||||
<MetronomeControls paused={isPlaying} bpm={bpm.current} />
|
||||
</View>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
PlayViewControlBar.defaultProps = {
|
||||
onResume: () => {},
|
||||
onPause: () => {},
|
||||
onEnd: () => {},
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default PlayViewControlBar;
|
||||
|
||||
@@ -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).toFixed(2)}%</Text>
|
||||
<Text fontSize="3xl">{Math.max(score, 0)}%</Text>
|
||||
<Row w="100%" style={{ justifyContent: 'space-between' }}>
|
||||
<Translate translationKey="precision" />
|
||||
<Text>{props.precision}%</Text>
|
||||
|
||||
119
front/components/SearchBar.tsx
Normal file
119
front/components/SearchBar.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
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;
|
||||
277
front/components/SearchResult.tsx
Normal file
277
front/components/SearchResult.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
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 />
|
||||
);
|
||||
};
|
||||
@@ -2,15 +2,17 @@ import * as React from 'react';
|
||||
import { Progress } from 'native-base';
|
||||
import { View, ViewStyle, StyleProp } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { scoreAtom } from './Play/PlayScore';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
export interface StarProgressProps {
|
||||
value: number;
|
||||
max: number;
|
||||
starSteps: number[];
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const StarProgress = (props: StarProgressProps) => {
|
||||
const [score] = useAtom(scoreAtom);
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -29,15 +31,15 @@ const StarProgress = (props: StarProgressProps) => {
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
value={props.value}
|
||||
value={score}
|
||||
max={props.max}
|
||||
/>
|
||||
{props.starSteps.map((step) => {
|
||||
return (
|
||||
<Ionicons
|
||||
key={step}
|
||||
name={step <= props.value ? 'star' : 'star-outline'}
|
||||
color={step <= props.value ? '#EBDA3C' : '#6075F9'}
|
||||
name={step <= score ? 'star' : 'star-outline'}
|
||||
color={step <= score ? '#EBDA3C' : '#6075F9'}
|
||||
size={20}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useMemo, memo } from 'react';
|
||||
import { StyleSheet, ViewStyle, Image, Platform } from 'react-native';
|
||||
import {
|
||||
Column,
|
||||
HStack,
|
||||
Row,
|
||||
Stack,
|
||||
Text,
|
||||
useBreakpointValue,
|
||||
useTheme,
|
||||
IconButton,
|
||||
} from 'native-base';
|
||||
import { StyleSheet, ViewStyle, Image } from 'react-native';
|
||||
import { Column, HStack, Row, Stack, Text, useBreakpointValue, useTheme } from 'native-base';
|
||||
import { HeartAdd, HeartRemove, Play } from 'iconsax-react-native';
|
||||
import WebIconButton from './IconButton';
|
||||
import IconButton from './IconButton';
|
||||
import Spacer from '../../components/UI/Spacer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -112,15 +103,18 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
backgroundColor: colors.coolGray[500],
|
||||
paddingRight: screenSize === 'small' ? 8 : 16,
|
||||
},
|
||||
playButton: {
|
||||
backgroundColor: colors.primary[300],
|
||||
borderRadius: 999,
|
||||
playButtonContainer: {
|
||||
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,
|
||||
},
|
||||
@@ -130,10 +124,6 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
},
|
||||
songContainer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
},
|
||||
stats: {
|
||||
display: 'flex',
|
||||
@@ -154,61 +144,35 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
return (
|
||||
<HStack space={screenSize === 'xl' ? 2 : 1} style={[styles.container, props.style]}>
|
||||
<Stack style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Image source={{ uri: props.image }} style={styles.image} />
|
||||
<IconButton
|
||||
containerStyle={styles.playButtonContainer}
|
||||
style={styles.playButton}
|
||||
padding={8}
|
||||
onPress={props.onPlay}
|
||||
icon={<Play size={24} variant="Bold" color="#FFF" />}
|
||||
color="#FFF"
|
||||
icon={Play}
|
||||
variant="Bold"
|
||||
size={24}
|
||||
/>
|
||||
<Image source={{ uri: props.image }} style={styles.image} />
|
||||
</Stack>
|
||||
<Column style={{ flex: 4, width: '100%', justifyContent: 'center' }}>
|
||||
<Text isTruncated numberOfLines={1} style={styles.artistText}>
|
||||
<Text numberOfLines={1} style={styles.artistText}>
|
||||
{props.artist}
|
||||
</Text>
|
||||
{screenSize === 'xl' && <Spacer height="xs" />}
|
||||
<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}
|
||||
iconActive={HeartRemove}
|
||||
activeVariant="Bold"
|
||||
size={screenSize === 'xl' ? 24 : 18}
|
||||
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 space={2} style={styles.songContainer}>
|
||||
<Text numberOfLines={1}>{props.song}</Text>
|
||||
<IconButton
|
||||
colorActive={colors.text[700]}
|
||||
color={colors.primary[300]}
|
||||
icon={HeartAdd}
|
||||
iconActive={HeartRemove}
|
||||
activeVariant="Bold"
|
||||
size={screenSize === 'xl' ? 24 : 18}
|
||||
isActive={props.liked}
|
||||
onPress={props.onLike}
|
||||
/>
|
||||
</Row>
|
||||
</Column>
|
||||
{[formattedLastScore, formattedBestScore].map((value, index) => (
|
||||
|
||||
@@ -132,9 +132,7 @@ function MusicListCC({
|
||||
style={{ marginBottom: 2 }}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => {
|
||||
return `${item.id}`;
|
||||
}}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
ListFooterComponent={
|
||||
hasMore ? (
|
||||
<View style={styles.footerContainer}>
|
||||
@@ -159,7 +157,7 @@ function MusicListCC({
|
||||
// Styles for the MusicList component
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
borderRadius: 10,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -39,7 +38,6 @@ 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')
|
||||
@@ -86,7 +84,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
)}
|
||||
</Row>
|
||||
<ButtonBase
|
||||
title={t('guestMode')}
|
||||
title={translate('guestMode')}
|
||||
onPress={async () => {
|
||||
try {
|
||||
handleGuestLogin((accessToken: string) => {
|
||||
@@ -154,7 +152,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
title={translate('continuewithgoogle')}
|
||||
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
|
||||
/>
|
||||
<SeparatorBase>{t('authOrSection')}</SeparatorBase>
|
||||
<SeparatorBase>or</SeparatorBase>
|
||||
<Stack
|
||||
space={3}
|
||||
justifyContent="center"
|
||||
|
||||
@@ -9,7 +9,7 @@ interface TextFormFieldProps extends TextFieldBaseProps {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const ERROR_HEIGHT = 25;
|
||||
const ERROR_HEIGHT = 20;
|
||||
const ERROR_PADDING_TOP = 8;
|
||||
|
||||
const TextFormField: React.FC<TextFormFieldProps> = ({ error, style, ...textFieldBaseProps }) => {
|
||||
@@ -49,12 +49,7 @@ const TextFormField: React.FC<TextFormFieldProps> = ({ error, style, ...textFiel
|
||||
}}
|
||||
>
|
||||
<Warning2 size="16" color="#f7253d" variant="Bold" />
|
||||
<Text
|
||||
isTruncated
|
||||
maxW={'100%'}
|
||||
fontSize={styles.errorText.fontSize}
|
||||
color={styles.errorText.color}
|
||||
>
|
||||
<Text isTruncated maxW={'100%'} style={styles.errorText}>
|
||||
{error}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
@@ -67,14 +62,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="24" color={'#ED4A51'} />
|
||||
<CloseCircle size="32" color={'#ED4A51'} />
|
||||
) : (
|
||||
<AddSquare size="24" color={'#6075F9'} />
|
||||
<AddSquare size="32" color={'#6075F9'} />
|
||||
)}
|
||||
<Text>{props.name}</Text>
|
||||
</View>
|
||||
@@ -40,24 +40,15 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => void }) => {
|
||||
const SearchBarComponent = () => {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [genre, setGenre] = React.useState('');
|
||||
const [genre, setGenre] = React.useState({} as Genre | undefined);
|
||||
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
|
||||
@@ -66,48 +57,62 @@ const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => vo
|
||||
borderBottomColor: '#9E9E9E',
|
||||
display: 'flex',
|
||||
flexDirection: isMobileView ? 'column' : 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
margin: 5,
|
||||
padding: isMobileView ? 8 : 16,
|
||||
padding: 16,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{!!artist && (
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{artist && (
|
||||
<ArtistChipComponent
|
||||
onPress={() => setArtist('')}
|
||||
name={artist}
|
||||
selected={true}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View style={{ flexGrow: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
variant={'unstyled'}
|
||||
placeholder={translate('searchBarPlaceholder')}
|
||||
style={{ height: 30, flex: 1 }}
|
||||
style={{ width: '100%', height: 30 }}
|
||||
onChangeText={(value) => setQuery(value)}
|
||||
/>
|
||||
</View>
|
||||
<ButtonBase type="menu" icon={SearchNormal1} onPress={handleValidate} />
|
||||
<ButtonBase
|
||||
type="menu"
|
||||
icon={SearchNormal1}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
@@ -141,13 +146,6 @@ const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => vo
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
@@ -156,20 +154,15 @@ const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => vo
|
||||
</ScrollView>
|
||||
<View>
|
||||
<Select
|
||||
selectedValue={genre}
|
||||
selectedValue={genre?.name}
|
||||
placeholder={translate('genreFilter')}
|
||||
accessibilityLabel="Genre"
|
||||
onValueChange={(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,
|
||||
});
|
||||
setGenre(
|
||||
genresQuery.data?.find((genre) => {
|
||||
genre.name == itemValue;
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Select.Item label={translate('emptySelection')} value="" />
|
||||
|
||||
@@ -52,10 +52,12 @@ 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
|
||||
@@ -84,7 +86,14 @@ export const BoardRow = ({ userAvatarUrl, userPseudo, userLvl, index }: BoardRow
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text fontSize={16} textAlign={'center'}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{index}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -62,7 +62,15 @@ export const PodiumCard = ({
|
||||
</Avatar>
|
||||
</View>
|
||||
<View>
|
||||
<Text fontSize={20} numberOfLines={2} isTruncated>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: '500',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
numberOfLines={2}
|
||||
isTruncated
|
||||
>
|
||||
{userPseudo ?? '---'}
|
||||
</Text>
|
||||
<View
|
||||
@@ -74,7 +82,12 @@ export const PodiumCard = ({
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<Text fontSize={24} fontWeight={'bold'}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{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,8 +321,6 @@ 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 = {
|
||||
@@ -645,12 +643,10 @@ 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 Profil',
|
||||
updateProfile: 'Changer le Profile',
|
||||
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 = {
|
||||
@@ -984,6 +980,4 @@ 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',
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"fbjs": "^3.0.5",
|
||||
"i18next": "^23.5.1",
|
||||
"iconsax-react-native": "^0.0.8",
|
||||
"jotai": "^2.6.1",
|
||||
"native-base": "^3.4.28",
|
||||
"normalize-css-color": "^1.0.2",
|
||||
"react": "18.2.0",
|
||||
|
||||
@@ -18,7 +18,11 @@ const ForgotPasswordView = () => {
|
||||
return 'Error with email, please contact support';
|
||||
}
|
||||
}
|
||||
return <ForgotPasswordForm onSubmit={handleSubmit} />;
|
||||
return (
|
||||
<div>
|
||||
<ForgotPasswordForm onSubmit={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPasswordView;
|
||||
|
||||
@@ -29,21 +29,22 @@ const Leaderboardiew = () => {
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<ScrollView>
|
||||
<Text
|
||||
fontSize={20}
|
||||
fontWeight={'500'}
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: '500',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{translate('leaderBoardHeading')}
|
||||
</Text>
|
||||
<Text fontSize={14} fontWeight={'500'}>
|
||||
<Text
|
||||
style={{
|
||||
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, { updateCursor } from '../components/Play/PartitionMagic';
|
||||
import PartitionMagic from '../components/Play/PartitionMagic';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useTheme, useBreakpointValue } from 'native-base';
|
||||
@@ -25,7 +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';
|
||||
import { PlayEndModal } from '../components/Play/PlayEndModal';
|
||||
|
||||
type PlayViewProps = {
|
||||
songId: number;
|
||||
@@ -33,7 +33,6 @@ 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) => {
|
||||
@@ -65,24 +64,22 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
const isPhone = screenSize === 'small';
|
||||
const song = useQuery(API.getSong(songId, ['artist']), { staleTime: Infinity });
|
||||
const toast = useToast();
|
||||
const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
||||
// const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
||||
const webSocket = useRef<WebSocket>();
|
||||
const [paused, setPause] = useState<boolean>(true);
|
||||
const stopwatch = useStopwatch();
|
||||
// const [paused, setPause] = useState<boolean>(true);
|
||||
// const stopwatch = useStopwatch();
|
||||
// const [time, setTime] = useState(0);
|
||||
const time = useRef(0);
|
||||
const [endResult, setEndResult] = useState<unknown>();
|
||||
const [shouldPlay, setShouldPlay] = useState(false);
|
||||
// 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);
|
||||
// const [score, setScore] = useState(0); // Between 0 and 100
|
||||
// const getElapsedTime = () => stopwatch.getElapsedRunningTime();
|
||||
// const [readyToPlay, setReadyToPlay] = useState(false);
|
||||
// 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
|
||||
const [streak, setStreak] = useState(0);
|
||||
// const [streak, setStreak] = useState(0);
|
||||
const colorScheme = useColorScheme();
|
||||
const { colors } = useTheme();
|
||||
const statColor = colors.lightText;
|
||||
@@ -116,8 +113,6 @@ 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');
|
||||
@@ -137,7 +132,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
console.log('MIDI inputs', inputs);
|
||||
let endMsgReceived = false; // Used to know if to go to error screen when websocket closes
|
||||
|
||||
if (inputs.size <= 0) {
|
||||
if (inputs.size <= 10) {
|
||||
toast.show({ description: 'No MIDI Keyboard found' });
|
||||
return;
|
||||
}
|
||||
@@ -152,16 +147,8 @@ 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' });
|
||||
@@ -179,30 +166,19 @@ 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;
|
||||
|
||||
@@ -243,9 +219,8 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
};
|
||||
inputs.forEach((input) => {
|
||||
input.onmidimessage = (message) => {
|
||||
const { command, note, velocity } = parseMidiMessage(message);
|
||||
let keyIsPressed = command == 9;
|
||||
if (velocity == 0) keyIsPressed = false;
|
||||
const { command, note } = parseMidiMessage(message);
|
||||
const keyIsPressed = command == 9;
|
||||
|
||||
webSocket.current?.send(
|
||||
JSON.stringify({
|
||||
@@ -262,23 +237,16 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
toast.show({ description: `Failed to get MIDI access` });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (playType == 'practice') return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
time.current = getElapsedTime();
|
||||
updateCursor?.();
|
||||
updateTimeControlBar?.();
|
||||
}, 200);
|
||||
return () => clearInterval(interval);
|
||||
}, [playType]);
|
||||
|
||||
useEffect(() => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
|
||||
// const interval = setInterval(() => {
|
||||
// setTime(() => getElapsedTime()); // Countdown
|
||||
// }, 200);
|
||||
|
||||
return () => {
|
||||
ScreenOrientation.unlockAsync().catch(() => {});
|
||||
stopwatch.stop();
|
||||
// stopwatch.stop();
|
||||
// clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -323,12 +291,13 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<PopupCC isVisible={endResult != undefined}>
|
||||
<PlayEndModal />
|
||||
{/* <PopupCC isVisible={endResult != undefined}>
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(() => (endResult ? <ScoreModal {...(endResult as any)} /> : <></>))()
|
||||
endResult ? <ScoreModal {...(endResult as any)} /> : <></>
|
||||
}
|
||||
</PopupCC>
|
||||
</PopupCC> */}
|
||||
<PopupCC
|
||||
title={translate('selectPlayMode')}
|
||||
description={translate('selectPlayModeExplaination')}
|
||||
@@ -349,10 +318,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
style={{}}
|
||||
type="outlined"
|
||||
title={translate('practiceBtn')}
|
||||
onPress={async () => {
|
||||
setPlayType('practice');
|
||||
setShouldPlay(true);
|
||||
}}
|
||||
onPress={async () => setPlayType('practice')}
|
||||
/>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
@@ -410,7 +376,7 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
<PlayScore score={score} streak={streak} message={lastScoreMessage} />
|
||||
<PlayScore />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -422,42 +388,36 @@ const PlayView = ({ songId }: PlayViewProps) => {
|
||||
}}
|
||||
>
|
||||
<PartitionMagic
|
||||
playType={playType}
|
||||
shouldPlay={shouldPlay}
|
||||
timestamp={time}
|
||||
songID={song.data.id}
|
||||
onEndReached={() => {
|
||||
setTimeout(() => {
|
||||
onEnd();
|
||||
}, 200);
|
||||
}}
|
||||
onError={() => {
|
||||
console.log('error from partition magic');
|
||||
}}
|
||||
onReady={() => {
|
||||
console.log('ready from partition magic');
|
||||
setReadyToPlay(true);
|
||||
}}
|
||||
onPlay={onResume}
|
||||
onPause={onPause}
|
||||
// onEndReached={() => {
|
||||
// setTimeout(() => {
|
||||
// onEnd();
|
||||
// }, 200);
|
||||
// }}
|
||||
// onError={() => {
|
||||
// console.log('error from partition magic');
|
||||
// }}
|
||||
// onReady={() => {
|
||||
// console.log('ready from partition magic');
|
||||
// setReadyToPlay(true);
|
||||
// }}
|
||||
// onPlay={onResume}
|
||||
// onPause={onPause}
|
||||
/>
|
||||
</View>
|
||||
<PlayViewControlBar
|
||||
playType={playType}
|
||||
score={score}
|
||||
time={time}
|
||||
paused={paused}
|
||||
disabled={playType == null || !readyToPlay}
|
||||
// score={score}
|
||||
// time={time}
|
||||
// paused={paused}
|
||||
// disabled={playType == null || !readyToPlay}
|
||||
song={song.data}
|
||||
onEnd={onEnd}
|
||||
onPause={() => {
|
||||
setShouldPlay(false);
|
||||
}}
|
||||
onResume={() => {
|
||||
setTimeout(() => {
|
||||
setShouldPlay(true);
|
||||
}, 3000);
|
||||
}}
|
||||
// onEnd={onEnd}
|
||||
// onPause={() => {
|
||||
// setShouldPlay(false);
|
||||
// }}
|
||||
// onResume={() => {
|
||||
// setShouldPlay(true);
|
||||
// }}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
{colorScheme === 'dark' && (
|
||||
|
||||
@@ -34,7 +34,12 @@ const ProfileView = () => {
|
||||
const isBigScreen = layout.width > 650;
|
||||
|
||||
return (
|
||||
<Flex flex={1} padding={8}>
|
||||
<Flex
|
||||
flex={1}
|
||||
style={{
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -71,7 +76,12 @@ const ProfileView = () => {
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<Text fontSize={'xl'} isTruncated numberOfLines={2} flexShrink={1}>
|
||||
<Text
|
||||
fontSize={'xl'}
|
||||
isTruncated
|
||||
numberOfLines={2}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{userQuery.data.name}
|
||||
</Text>
|
||||
<ButtonBase
|
||||
@@ -81,15 +91,10 @@ const ProfileView = () => {
|
||||
/>
|
||||
</View>
|
||||
<Translate
|
||||
style={{ fontWeight: 'bold' }}
|
||||
style={{ paddingBottom: 10, 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',
|
||||
|
||||
109
front/views/SearchView.tsx
Normal file
109
front/views/SearchView.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
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,7 +6,6 @@ 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(
|
||||
@@ -17,6 +16,7 @@ const HomeView = () => {
|
||||
const isPhone = screenSize === 'small';
|
||||
const topSuggestions = suggestionsQuery.data?.slice(0, 4) ?? [];
|
||||
const suggestions = suggestionsQuery.data?.slice(4) ?? [];
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View
|
||||
@@ -25,7 +25,6 @@ const HomeView = () => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -47,15 +46,15 @@ const HomeView = () => {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
fontSize={24}
|
||||
fontWeight={'bold'}
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 16,
|
||||
marginBottom: 16,
|
||||
marginTop: 24,
|
||||
}}
|
||||
>
|
||||
{translate('discoverySuggestionSectionTitle')}
|
||||
{'Suggestions'}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -1,48 +1,12 @@
|
||||
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 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;
|
||||
};
|
||||
import { View } from 'react-native';
|
||||
|
||||
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: 20, padding: 8 }}>
|
||||
<SearchBarComponent onValidate={(query) => setSearchQuery(query)} />
|
||||
{rawResult.isSuccess ? (
|
||||
<MusicListCC
|
||||
musics={rawResult.data}
|
||||
isFetching={rawResult.isFetching}
|
||||
refetch={rawResult.refetch}
|
||||
/>
|
||||
) : (
|
||||
<SearchHistory />
|
||||
)}
|
||||
<View style={{ display: 'flex', gap: 50 }}>
|
||||
<SearchBarComponent />
|
||||
<SearchHistory />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7771,6 +7771,11 @@ join-component@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
||||
integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==
|
||||
|
||||
jotai@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.6.1.tgz#ece33a50b604e41b0134f94dd621e55d1bdc66f7"
|
||||
integrity sha512-GLQtAnA9iEKRMXnyCjf1azIxfQi5JausX2EI5qSlb59j4i73ZEyV/EXPDEAQj4uQNZYEefi3degv/Pw3+L/Dtg==
|
||||
|
||||
js-sha3@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
|
||||
@@ -4,7 +4,6 @@ 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,11 +17,6 @@ class StartMessage(ValidatedDC):
|
||||
mode: Literal["normal", "practice"]
|
||||
type: Literal["start"] = "start"
|
||||
|
||||
@dataclass
|
||||
class PingMessage(ValidatedDC):
|
||||
type: Literal["ping"] = "ping"
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class EndMessage(ValidatedDC):
|
||||
@@ -57,7 +52,6 @@ message_map = {
|
||||
"note_on": NoteOnMessage,
|
||||
"note_off": NoteOffMessage,
|
||||
"pause": PauseMessage,
|
||||
"ping": PingMessage,
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +62,7 @@ def getMessage() -> (
|
||||
| NoteOnMessage
|
||||
| NoteOffMessage
|
||||
| PauseMessage
|
||||
| InvalidMessage
|
||||
| PingMessage,
|
||||
| InvalidMessage,
|
||||
str,
|
||||
]
|
||||
):
|
||||
|
||||
@@ -9,9 +9,8 @@ from mido import MidiFile
|
||||
|
||||
RATIO = 1.0
|
||||
|
||||
def getPartition(midiFile: str, cursors) -> Partition:
|
||||
notes = [Key(i["note"], cursor["timestamp"], i["duration"]) for cursor in cursors for i in cursor["notes"]]
|
||||
'''
|
||||
def getPartition(midiFile: str) -> Partition:
|
||||
notes = []
|
||||
s = 0
|
||||
notes_on = {}
|
||||
prev_note_on = {}
|
||||
@@ -30,7 +29,6 @@ def getPartition(midiFile: str, cursors) -> 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,7 +17,6 @@ from chroma_case.Message import (
|
||||
NoteOffMessage,
|
||||
NoteOnMessage,
|
||||
PauseMessage,
|
||||
PingMessage,
|
||||
StartMessage,
|
||||
getMessage,
|
||||
)
|
||||
@@ -85,17 +84,15 @@ def send(o):
|
||||
|
||||
class Scorometer:
|
||||
def __init__(self, mode: int, midiFile: str, song_id: int, user_id: int) -> None:
|
||||
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.partition: Partition = getPartition(midiFile)
|
||||
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: list[Key] = []
|
||||
self.keys_down = []
|
||||
self.mode: int = mode
|
||||
self.song_id: int = song_id
|
||||
self.user_id: int = user_id
|
||||
self.wrong_ids = set()
|
||||
self.wrong_ids = []
|
||||
self.difficulties = {}
|
||||
self.info: ScoroInfo = {
|
||||
"max_score": len(self.partition.notes) * 100,
|
||||
@@ -109,14 +106,6 @@ 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"
|
||||
@@ -165,14 +154,13 @@ 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.add(message.id)
|
||||
self.wrong_ids += [message.id]
|
||||
logging.debug({"note_on": f"wrong key {message.note}"})
|
||||
self.send({"type": "timing", "id": message.id, "timing": "wrong"})
|
||||
|
||||
@@ -183,10 +171,8 @@ 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)
|
||||
@@ -209,24 +195,67 @@ 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})
|
||||
self.keys_down_practice.add(message.note)
|
||||
self.practiceCheck()
|
||||
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"})
|
||||
|
||||
def handleNoteOffPractice(self, message: NoteOffMessage):
|
||||
logging.debug({"note_off": message.note})
|
||||
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)])
|
||||
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"})
|
||||
|
||||
def getDurationScore(self, key: Key, to_play: Key):
|
||||
tempo_percent = abs((key.duration / to_play.duration) - 1)
|
||||
@@ -258,16 +287,13 @@ class Scorometer:
|
||||
| NoteOnMessage
|
||||
| NoteOffMessage
|
||||
| PauseMessage
|
||||
| InvalidMessage
|
||||
| PingMessage,
|
||||
| InvalidMessage,
|
||||
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)
|
||||
@@ -295,7 +321,7 @@ class Scorometer:
|
||||
|
||||
def endGame(self):
|
||||
for i in self.partition.notes:
|
||||
if i.done is False and not i.half_done:
|
||||
if i.done is False:
|
||||
self.info["score"] -= 25
|
||||
self.info["missed"] += 1
|
||||
send(
|
||||
@@ -319,17 +345,6 @@ class Scorometer:
|
||||
headers=auth_header
|
||||
)
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user