6 Commits

Author SHA1 Message Date
GitBluub
10f033fe78 wip: password reset routes 2023-09-15 15:11:14 +02:00
869b2e696f Update .env.example 2023-09-13 17:32:10 +02:00
050c970e7e Add a button to resend verified mail 2023-09-13 17:30:34 +02:00
5c83235cba Add verified badge and page on the front 2023-09-13 17:25:01 +02:00
3b2ca9963b Use a fixed python version for the scorometer 2023-09-11 16:13:28 +02:00
0a08193418 Send mails on account creation 2023-09-07 16:58:18 +02:00
21 changed files with 186 additions and 356 deletions

View File

@@ -10,6 +10,5 @@ SCORO_URL=ws://localhost:6543
GOOGLE_CLIENT_ID=toto GOOGLE_CLIENT_ID=toto
GOOGLE_SECRET=tata GOOGLE_SECRET=tata
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
SMTP_TRANSPORT=smtps://toto:tata@relay SMTP_TRANSPORT=
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>' MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
IGNORE_MAILS=true

View File

@@ -93,7 +93,7 @@ jobs:
run: | run: |
docker-compose ps -a docker-compose ps -a
docker-compose logs docker-compose logs
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1) wget --retry-connrefused http://localhost:3000 # /healthcheck
- name: Run scorometer tests - name: Run scorometer tests
run: | run: |

1
.gitignore vendored
View File

@@ -13,4 +13,3 @@ log.html
node_modules/ node_modules/
./front/coverage ./front/coverage
.venv .venv
.DS_Store

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3 #!/bin/env python3
import sys import sys
import os import os

View File

@@ -79,6 +79,22 @@ export class AuthController {
} }
} }
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('reset')
async password_reset(@Request() req: any, @Query('token') token: string): Promise<void> {
if (await this.authService.resetPassword(req.user.id, token))
return;
throw new BadRequestException("Invalid token. Expired or invalid.");
}
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('send-reset')
async send_reset(@Request() req: any): Promise<void> {
await this.authService.sendResetMail(req.user);
}
@HttpCode(200) @HttpCode(200)
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Put('verify') @Put('verify')

View File

@@ -36,7 +36,6 @@ export class AuthService {
} }
async sendVerifyMail(user: User) { async sendVerifyMail(user: User) {
if (process.env.IGNORE_MAILS === "true") return;
const token = await this.jwtService.signAsync( const token = await this.jwtService.signAsync(
{ {
userId: user.id, userId: user.id,

View File

@@ -6,14 +6,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
export class SongService { export class SongService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
async songByArtist(data: number): Promise<Song[]> {
return this.prisma.song.findMany({
where: {
artistId: {equals: data},
},
});
}
async createSong(data: Prisma.SongCreateInput): Promise<Song> { async createSong(data: Prisma.SongCreateInput): Promise<Song> {
return this.prisma.song.create({ return this.prisma.song.create({
data, data,

View File

@@ -21,7 +21,7 @@ import { PlageHandler } from './models/Plage';
import { ListHandler } from './models/List'; import { ListHandler } from './models/List';
import { AccessTokenResponseHandler } from './models/AccessTokenResponse'; import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
import * as yup from 'yup'; import * as yup from 'yup';
import { base64ToBlob } from './utils/base64ToBlob'; import { base64ToBlob } from 'file64';
import { ImagePickerAsset } from 'expo-image-picker'; import { ImagePickerAsset } from 'expo-image-picker';
type AuthenticationInput = { username: string; password: string }; type AuthenticationInput = { username: string; password: string };
@@ -287,43 +287,6 @@ export default class API {
), ),
}; };
} }
/**
* @description retrieves songs from a specific artist
* @param artistId is the id of the artist that composed the songs aimed
* @returns a Promise of Songs type array
*/
public static getSongsByArtist(artistId: number): Query<Song[]> {
return {
key: ['artist', artistId, 'songs'],
exec: () =>
API.fetch(
{
route: `/song?artistId=${artistId}`,
},
{ handler: PlageHandler(SongHandler) }
).then(({ data }) => data),
};
}
/**
* Retrieves all songs corresponding to the given genre ID
* @param genreId the id of the genre we're aiming
* @returns a promise of an array of Songs
*/
public static getSongsByGenre(genreId: number): Query<Song[]> {
return {
key: ['genre', genreId, 'songs'],
exec: () =>
API.fetch(
{
route: `/song?genreId=${genreId}`,
},
{ handler: PlageHandler(SongHandler) }
).then(({ data }) => data),
};
}
/** /**
* Retrive a song's midi partition * Retrive a song's midi partition
* @param songId the id to find the song * @param songId the id to find the song
@@ -359,23 +322,6 @@ export default class API {
return `${API.baseUrl}/genre/${genreId}/illustration`; return `${API.baseUrl}/genre/${genreId}/illustration`;
} }
/**
* Retrieves a genre
* @param genreId the id of the aimed genre
*/
public static getGenre(genreId: number): Query<Genre> {
return {
key: ['genre', genreId],
exec: () =>
API.fetch(
{
route: `/genre/${genreId}`,
},
{ handler: GenreHandler }
),
};
}
/** /**
* Retrive a song's musicXML partition * Retrive a song's musicXML partition
* @param songId the id to find the song * @param songId the id to find the song

View File

@@ -28,7 +28,6 @@ import { Button, Center, VStack } from 'native-base';
import { unsetAccessToken } from './state/UserSlice'; import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton'; import TextButton from './components/TextButton';
import ErrorView from './views/ErrorView'; import ErrorView from './views/ErrorView';
import GenreDetailsView from './views/GenreDetailsView';
import GoogleView from './views/GoogleView'; import GoogleView from './views/GoogleView';
import VerifiedView from './views/VerifiedView'; import VerifiedView from './views/VerifiedView';
@@ -61,11 +60,6 @@ const protectedRoutes = () =>
options: { title: translate('artistFilter') }, options: { title: translate('artistFilter') },
link: '/artist/:artistId', link: '/artist/:artistId',
}, },
Genre: {
component: GenreDetailsView,
options: { title: translate('genreFilter') },
link: '/genre/:genreId',
},
Score: { Score: {
component: ScoreView, component: ScoreView,
options: { title: translate('score'), headerLeft: null }, options: { title: translate('score'), headerLeft: null },

View File

@@ -1,34 +0,0 @@
import { useColorScheme } from 'react-native';
import { RootState, useSelector } from '../state/Store';
import { Box, Pressable } from 'native-base';
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
const settings = useSelector((state: RootState) => state.settings.local);
const systemColorMode = useColorScheme();
const colorScheme = settings.colorScheme;
return (
<Pressable onPress={props.onPress}>
{({ isHovered, isPressed }) => (
<Box
{...props}
py={3}
my={1}
bg={
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
? isHovered || isPressed
? 'gray.800'
: undefined
: isHovered || isPressed
? 'coolGray.200'
: undefined
}
>
{props.children}
</Box>
)}
</Pressable>
);
};
export default RowCustom;

View File

@@ -1,16 +1,20 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { import {
HStack,
VStack, VStack,
Heading, Heading,
Text, Text,
Pressable,
Box, Box,
Card, Card,
Image,
Flex, Flex,
useBreakpointValue, useBreakpointValue,
Column, Column,
ScrollView, ScrollView,
} from 'native-base'; } from 'native-base';
import { SafeAreaView } from 'react-native'; import { SafeAreaView, useColorScheme } from 'react-native';
import { RootState, useSelector } from '../state/Store';
import { SearchContext } from '../views/SearchView'; import { SearchContext } from '../views/SearchView';
import { useQueries, useQuery } from '../Queries'; import { useQueries, useQuery } from '../Queries';
import { translate } from '../i18n/i18n'; import { translate } from '../i18n/i18n';
@@ -20,11 +24,11 @@ import ArtistCard from './ArtistCard';
import GenreCard from './GenreCard'; import GenreCard from './GenreCard';
import SongCard from './SongCard'; import SongCard from './SongCard';
import CardGridCustom from './CardGridCustom'; import CardGridCustom from './CardGridCustom';
import TextButton from './TextButton';
import SearchHistoryCard from './HistoryCard'; import SearchHistoryCard from './HistoryCard';
import Song, { SongWithArtist } from '../models/Song'; import Song, { SongWithArtist } from '../models/Song';
import { useNavigation } from '../Navigation'; import { useNavigation } from '../Navigation';
import Artist from '../models/Artist'; import Artist from '../models/Artist';
import SongRow from '../components/SongRow';
const swaToSongCardProps = (song: SongWithArtist) => ({ const swaToSongCardProps = (song: SongWithArtist) => ({
songId: song.id, songId: song.id,
@@ -33,6 +37,101 @@ const swaToSongCardProps = (song: SongWithArtist) => ({
cover: song.cover ?? 'https://picsum.photos/200', cover: song.cover ?? 'https://picsum.photos/200',
}); });
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
const settings = useSelector((state: RootState) => state.settings.local);
const systemColorMode = useColorScheme();
const colorScheme = settings.colorScheme;
return (
<Pressable onPress={props.onPress}>
{({ isHovered, isPressed }) => (
<Box
{...props}
py={3}
my={1}
bg={
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
? isHovered || isPressed
? 'gray.800'
: undefined
: isHovered || isPressed
? 'coolGray.200'
: undefined
}
>
{props.children}
</Box>
)}
</Pressable>
);
};
type SongRowProps = {
song: Song | SongWithArtist; // TODO: remove Song
onPress: () => void;
};
const SongRow = ({ song, onPress }: SongRowProps) => {
return (
<RowCustom width={'100%'}>
<HStack px={2} space={5} justifyContent={'space-between'}>
<Image
flexShrink={0}
flexGrow={0}
pl={10}
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
source={{ uri: song.cover }}
alt={song.name}
/>
<HStack
style={{
display: 'flex',
flexShrink: 1,
flexGrow: 1,
alignItems: 'center',
justifyContent: 'flex-start',
}}
space={6}
>
<Text
style={{
flexShrink: 1,
}}
isTruncated
pl={10}
maxW={'100%'}
bold
fontSize="md"
>
{song.name}
</Text>
<Text
style={{
flexShrink: 0,
}}
fontSize={'sm'}
>
{song.artistId ?? 'artist'}
</Text>
</HStack>
<TextButton
flexShrink={0}
flexGrow={0}
translate={{ translationKey: 'playBtn' }}
colorScheme="primary"
variant={'outline'}
size="sm"
onPress={onPress}
/>
</HStack>
</RowCustom>
);
};
SongRow.defaultProps = {
onPress: () => {},
};
const HomeSearchComponent = () => { const HomeSearchComponent = () => {
const { updateStringQuery } = React.useContext(SearchContext); const { updateStringQuery } = React.useContext(SearchContext);
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery( const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
@@ -153,17 +252,15 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
</Text> </Text>
{artistData?.length ? ( {artistData?.length ? (
<CardGridCustom <CardGridCustom
content={artistData content={artistData.slice(0, props.maxItems ?? artistData.length).map((a) => ({
.slice(0, props.maxItems ?? artistData.length) image: API.getArtistIllustration(a.id),
.map((artistData) => ({ name: a.name,
image: API.getArtistIllustration(artistData.id), id: a.id,
name: artistData.name, onPress: () => {
id: artistData.id, API.createSearchHistoryEntry(a.name, 'artist');
onPress: () => { navigation.navigate('Artist', { artistId: a.id });
API.createSearchHistoryEntry(artistData.name, 'artist'); },
navigation.navigate('Artist', { artistId: artistData.id }); }))}
},
}))}
cardComponent={ArtistCard} cardComponent={ArtistCard}
/> />
) : ( ) : (
@@ -190,7 +287,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
id: g.id, id: g.id,
onPress: () => { onPress: () => {
API.createSearchHistoryEntry(g.name, 'genre'); API.createSearchHistoryEntry(g.name, 'genre');
navigation.navigate('Genre', { genreId: g.id }); navigation.navigate('Home');
}, },
}))} }))}
cardComponent={GenreCard} cardComponent={GenreCard}

View File

@@ -1,71 +0,0 @@
import { HStack, Image, Text } from 'native-base';
import Song, { SongWithArtist } from '../models/Song';
import RowCustom from './RowCustom';
import TextButton from './TextButton';
type SongRowProps = {
song: Song | SongWithArtist; // TODO: remove Song
onPress: () => void;
};
const SongRow = ({ song, onPress }: SongRowProps) => {
return (
<RowCustom width={'100%'}>
<HStack px={2} space={5} justifyContent={'space-between'}>
<Image
flexShrink={0}
flexGrow={0}
pl={10}
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
source={{ uri: song.cover }}
alt={song.name}
borderColor={'white'}
borderWidth={1}
/>
<HStack
style={{
display: 'flex',
flexShrink: 1,
flexGrow: 1,
alignItems: 'center',
justifyContent: 'flex-start',
}}
space={6}
>
<Text
style={{
flexShrink: 1,
}}
isTruncated
pl={5}
maxW={'100%'}
bold
fontSize="md"
>
{song.name}
</Text>
<Text
style={{
flexShrink: 0,
}}
fontSize={'sm'}
>
{song.artistId ?? 'artist'}
</Text>
</HStack>
<TextButton
flexShrink={0}
flexGrow={0}
translate={{ translationKey: 'playBtn' }}
colorScheme="primary"
variant={'outline'}
size="sm"
mr={5}
onPress={onPress}
/>
</HStack>
</RowCustom>
);
};
export default SongRow;

View File

@@ -18,9 +18,10 @@ const UserAvatar = ({ size }: UserAvatarProps) => {
if (!user.data) { if (!user.data) {
return null; return null;
} }
// NOTE: We do this to avoid parsing URL with `new URL`, which is not compatible with related path const url = new URL(user.data.data.avatar);
// (which is used for production, on web)
return `${user.data.data.avatar}?updatedAt=${user.dataUpdatedAt.toString()}`; url.searchParams.append('updatedAt', user.dataUpdatedAt.toString());
return url;
}, [user.data]); }, [user.data]);
return ( return (

View File

@@ -43,7 +43,6 @@ export const en = {
artistFilter: 'Artists', artistFilter: 'Artists',
songsFilter: 'Songs', songsFilter: 'Songs',
genreFilter: 'Genres', genreFilter: 'Genres',
favoriteFilter: 'Favorites',
// profile page // profile page
user: 'Profile', user: 'Profile',
@@ -183,7 +182,7 @@ export const en = {
noRecentSearches: 'No recent searches', noRecentSearches: 'No recent searches',
avatar: 'Avatar', avatar: 'Avatar',
changeIt: 'Change It', changeIt: 'Change It',
verified: 'Verified', verified: "Verified",
}; };
export const fr: typeof en = { export const fr: typeof en = {
@@ -233,7 +232,6 @@ export const fr: typeof en = {
artistFilter: 'Artistes', artistFilter: 'Artistes',
songsFilter: 'Morceaux', songsFilter: 'Morceaux',
genreFilter: 'Genres', genreFilter: 'Genres',
favoriteFilter: 'Favoris',
// Difficulty settings // Difficulty settings
diffBtn: 'Difficulté', diffBtn: 'Difficulté',
@@ -369,7 +367,7 @@ export const fr: typeof en = {
noRecentSearches: 'Aucune recherche récente', noRecentSearches: 'Aucune recherche récente',
avatar: 'Avatar', avatar: 'Avatar',
changeIt: 'Modifier', changeIt: 'Modifier',
verified: 'Verifié', verified: "Verifié",
}; };
export const sp: typeof en = { export const sp: typeof en = {
@@ -432,7 +430,6 @@ export const sp: typeof en = {
artistFilter: 'Artistas', artistFilter: 'Artistas',
songsFilter: 'canciones', songsFilter: 'canciones',
genreFilter: 'géneros', genreFilter: 'géneros',
favoriteFilter: 'Favorites',
// Difficulty settings // Difficulty settings
diffBtn: 'Dificultad', diffBtn: 'Dificultad',
@@ -560,5 +557,5 @@ export const sp: typeof en = {
avatar: 'Avatar', avatar: 'Avatar',
changeIt: 'Cambialo', changeIt: 'Cambialo',
verified: 'Verified', verified: "Verified"
}; };

View File

@@ -8,8 +8,8 @@
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"eject": "expo eject", "eject": "expo eject",
"pretty:check": "prettier --check .", "pretty:check": "prettier --check",
"pretty:write": "prettier --write .", "pretty:write": "prettier --write",
"lint": "eslint .", "lint": "eslint .",
"test": "jest -i", "test": "jest -i",
"test:cov": "jest -i --coverage", "test:cov": "jest -i --coverage",
@@ -40,6 +40,7 @@
"expo-secure-store": "~12.0.0", "expo-secure-store": "~12.0.0",
"expo-splash-screen": "~0.17.5", "expo-splash-screen": "~0.17.5",
"expo-status-bar": "~1.4.2", "expo-status-bar": "~1.4.2",
"file64": "^1.0.2",
"format-duration": "^2.0.0", "format-duration": "^2.0.0",
"i18next": "^21.8.16", "i18next": "^21.8.16",
"install": "^0.13.0", "install": "^0.13.0",

View File

@@ -1,23 +0,0 @@
// SRC: https://github.com/encrypit/file64/blob/master/src/base64-to-blob.ts
export async function base64ToBlob(base64: string): Promise<Blob> {
const response = await fetch(base64);
let blob = await response.blob();
const mimeType = getMimeType(base64);
if (mimeType) {
// https://stackoverflow.com/a/50875615
blob = blob.slice(0, blob.size, mimeType);
}
return blob;
}
const mimeRegex = /^data:(.+);base64,/;
/**
* Gets MIME type from Base64.
*
* @param base64 - Base64.
* @returns - MIME type.
*/
function getMimeType(base64: string) {
return base64.match(mimeRegex)?.slice(1, 2).pop();
}

View File

@@ -1,58 +1,49 @@
import { Box, Heading, useBreakpointValue, ScrollView } from 'native-base'; import { VStack, Image, Heading, IconButton, Icon, Container } from 'native-base';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native';
import { useQuery } from '../Queries'; import { useQuery } from '../Queries';
import { LoadingView } from '../components/Loading'; import { LoadingView } from '../components/Loading';
import API from '../API'; import API from '../API';
import Song from '../models/Song'; import { useNavigation } from '../Navigation';
import SongRow from '../components/SongRow';
import { Key } from 'react'; const handleFavorite = () => {};
import { RouteProps, useNavigation } from '../Navigation';
import { ImageBackground } from 'react-native';
type ArtistDetailsViewProps = { type ArtistDetailsViewProps = {
artistId: number; artistId: number;
}; };
const ArtistDetailsView = ({ artistId }: RouteProps<ArtistDetailsViewProps>) => { const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => {
const artistQuery = useQuery(API.getArtist(artistId));
const songsQuery = useQuery(API.getSongsByArtist(artistId));
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isMobileView = screenSize == 'small';
const navigation = useNavigation(); const navigation = useNavigation();
const { isLoading, data: artistData, isError } = useQuery(API.getArtist(artistId));
if (artistQuery.isError || songsQuery.isError) { if (isLoading) {
navigation.navigate('Error');
return <></>;
}
if (!artistQuery.data || songsQuery.data === undefined) {
return <LoadingView />; return <LoadingView />;
} }
if (isError) {
navigation.navigate('Error');
}
return ( return (
<ScrollView> <SafeAreaView>
<ImageBackground <Container m={3}>
style={{ width: '100%', height: isMobileView ? 200 : 300 }} <Image
source={{ uri: API.getArtistIllustration(artistQuery.data.id) }} source={{ uri: 'https://picsum.photos/200' }}
></ImageBackground> alt={artistData?.name}
<Box> size={20}
<Heading mt={-20} ml={3} fontSize={50}> borderRadius="full"
{artistQuery.data.name} />
</Heading> <VStack space={3}>
<ScrollView mt={3}> <Heading>{artistData?.name}</Heading>
<Box> <IconButton
{songsQuery.data.map((comp: Song, index: Key | null | undefined) => ( icon={<Icon as={Ionicons} name="heart" size={6} color="red.500" />}
<SongRow onPress={() => handleFavorite()}
key={index} variant="unstyled"
song={comp} _pressed={{ opacity: 0.6 }}
onPress={() => { />
API.createSearchHistoryEntry(comp.name, 'song'); </VStack>
navigation.navigate('Song', { songId: comp.id }); </Container>
}} </SafeAreaView>
/>
))}
</Box>
</ScrollView>
</Box>
</ScrollView>
); );
}; };

View File

@@ -1,74 +0,0 @@
import { Flex, Heading, useBreakpointValue, ScrollView } from 'native-base';
import { useQueries, useQuery } from '../Queries';
import { LoadingView } from '../components/Loading';
import { RouteProps, useNavigation } from '../Navigation';
import API from '../API';
import CardGridCustom from '../components/CardGridCustom';
import SongCard from '../components/SongCard';
import { ImageBackground } from 'react-native';
type GenreDetailsViewProps = {
genreId: number;
};
const GenreDetailsView = ({ genreId }: RouteProps<GenreDetailsViewProps>) => {
const genreQuery = useQuery(API.getGenre(genreId));
const songsQuery = useQuery(API.getSongsByGenre(genreId));
const artistQueries = useQueries(
songsQuery.data?.map((song) => song.artistId).map((artistId) => API.getArtist(artistId)) ??
[]
);
// Here, .artist will always be defined
const songWithArtist = songsQuery?.data
?.map((song) => ({
...song,
artist: artistQueries.find((query) => query.data?.id == song.artistId)?.data,
}))
.filter((song) => song.artist !== undefined);
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isMobileView = screenSize == 'small';
const navigation = useNavigation();
if (genreQuery.isError || songsQuery.isError) {
navigation.navigate('Error');
return <></>;
}
if (!genreQuery.data || songsQuery.data === undefined || songWithArtist === undefined) {
return <LoadingView />;
}
return (
<ScrollView>
<ImageBackground
style={{ width: '100%', height: isMobileView ? 200 : 300 }}
source={{ uri: API.getGenreIllustration(genreQuery.data.id) }}
></ImageBackground>
<Heading ml={3} fontSize={50}>
{genreQuery.data.name}
</Heading>
<Flex
flexWrap="wrap"
direction={isMobileView ? 'column' : 'row'}
justifyContent={['flex-start']}
mt={4}
>
<CardGridCustom
content={songWithArtist.map((songData) => ({
name: songData.name,
cover: songData.cover,
artistName: songData.artist!.name,
songId: songData.id,
onPress: () => {
API.createSearchHistoryEntry(songData.name, 'song');
navigation.navigate('Song', { songId: songData.id });
},
}))}
cardComponent={SongCard}
/>
</Flex>
</ScrollView>
);
};
export default GenreDetailsView;

View File

@@ -77,7 +77,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const webSocket = useRef<WebSocket>(); const webSocket = useRef<WebSocket>();
const [paused, setPause] = useState<boolean>(true); const [paused, setPause] = useState<boolean>(true);
const stopwatch = useStopwatch(); const stopwatch = useStopwatch();
const time = useRef(0); const [time, setTime] = useState(0);
const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render
const [score, setScore] = useState(0); // Between 0 and 100 const [score, setScore] = useState(0); // Between 0 and 100
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current;
@@ -236,7 +236,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
useEffect(() => { useEffect(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {}); ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
const interval = setInterval(() => { const interval = setInterval(() => {
time.current = getElapsedTime(); // Countdown setTime(() => getElapsedTime()); // Countdown
}, 1); }, 1);
return () => { return () => {
@@ -289,7 +289,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
<View style={{ flexGrow: 1, justifyContent: 'center' }}> <View style={{ flexGrow: 1, justifyContent: 'center' }}>
<PartitionCoord <PartitionCoord
file={musixml.data} file={musixml.data}
timestamp={time.current} timestamp={time}
onEndReached={onEnd} onEndReached={onEnd}
onPause={onPause} onPause={onPause}
onResume={onResume} onResume={onResume}
@@ -342,14 +342,14 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
}} }}
/> />
<Text> <Text>
{time.current < 0 {time < 0
? paused ? paused
? '0:00' ? '0:00'
: Math.floor((time.current % 60000) / 1000) : Math.floor((time % 60000) / 1000)
.toFixed(0) .toFixed(0)
.toString() .toString()
: `${Math.floor(time.current / 60000)}:${Math.floor( : `${Math.floor(time / 60000)}:${Math.floor(
(time.current % 60000) / 1000 (time % 60000) / 1000
) )
.toFixed(0) .toFixed(0)
.toString() .toString()

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import API from '../API'; import API from '../API';
import { Text } from 'native-base'; import { Text } from 'native-base';
import { useNavigation } from '../Navigation'; import { useNavigation } from '../Navigation';
@@ -13,7 +14,6 @@ const VerifiedView = () => {
async function run() { async function run() {
try { try {
await API.fetch({ await API.fetch({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
route: `/auth/verify?token=${(route.params as any).token}`, route: `/auth/verify?token=${(route.params as any).token}`,
method: 'PUT', method: 'PUT',
}); });

View File

@@ -9118,11 +9118,6 @@ expo-keep-awake@~11.0.1:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3" resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A== integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==
expo-linear-gradient@^12.3.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.3.0.tgz#7abd8fedbf0138c86805aebbdfbbf5e5fa865f19"
integrity sha512-f9e+Oxe5z7fNQarTBZXilMyswlkbYWQHONVfq8MqmiEnW3h9XsxxmVJLG8uVQSQPUsbW+x1UUT/tnU6mkMWeLg==
expo-linking@~3.3.1: expo-linking@~3.3.1:
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357" resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"
@@ -9456,6 +9451,11 @@ file-uri-to-path@1.0.0:
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
file64@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/file64/-/file64-1.0.2.tgz#d3dde9bab142ccf0049e0bd407a2576e94894825"
integrity sha512-cDQefGBdb8OO7Pb2nXiRcZlVjwgzoG0uuJ/H2fxNdz3vbOZctp0iPJoHDQ4VZrirqGYc9n/p9+ZqptLZrcSGRA==
filesize@6.1.0: filesize@6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00"