Compare commits
29 Commits
v0.8.4
...
guest-user
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cec07b7e99 | ||
|
|
f93968c3eb | ||
|
|
f80253cea3 | ||
|
|
60a73781bd | ||
|
|
4e3b378d6a | ||
|
|
2bf1e783a9 | ||
|
|
375d36f6c5 | ||
|
|
495380ec43 | ||
|
|
af0531bb0c | ||
|
|
c5124fa6ad | ||
|
|
962cf58e77 | ||
|
|
60988dd599 | ||
|
|
004a541302 | ||
|
|
f4cd9e18ea | ||
|
|
2dc301addf | ||
|
|
e85a959c26 | ||
|
|
339e808d27 | ||
|
|
22d1a97abd | ||
|
|
ce4baa61dc | ||
|
|
e90c7f05a8 | ||
|
|
fb0e43af88 | ||
|
|
4577997b1c | ||
|
|
9bb256f2ee | ||
|
|
d3994ff26e | ||
|
|
00d097f643 | ||
|
|
99da77f23e | ||
|
|
7a6dc8b0c9 | ||
|
|
b4f04f9b71 | ||
|
|
9df0c98100 |
@@ -51,6 +51,7 @@ import { PasswordResetDto } from "./dto/password_reset.dto ";
|
||||
import { mapInclude } from "src/utils/include";
|
||||
import { SongController } from "src/song/song.controller";
|
||||
import { ChromaAuthGuard } from "./chroma-auth.guard";
|
||||
import { GuestDto } from "./dto/guest.dto";
|
||||
|
||||
@ApiTags("auth")
|
||||
@Controller("auth")
|
||||
@@ -162,8 +163,8 @@ export class AuthController {
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: "Login as a guest account" })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||
async guest(): Promise<JwtToken> {
|
||||
const user = await this.usersService.createGuest();
|
||||
async guest(@Body() guestdto: GuestDto): Promise<JwtToken> {
|
||||
const user = await this.usersService.createGuest(guestdto.username);
|
||||
await this.settingsService.createUserSetting(user.id);
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
8
back/src/auth/dto/guest.dto.ts
Normal file
8
back/src/auth/dto/guest.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class GuestDto {
|
||||
@ApiProperty()
|
||||
@IsNotEmpty()
|
||||
username: string;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { User, Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import * as bcrypt from "bcryptjs";
|
||||
import { createHash, randomUUID } from "crypto";
|
||||
import { createHash } from "crypto";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
@@ -46,10 +46,10 @@ export class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
async createGuest(): Promise<User> {
|
||||
async createGuest(displayName: string): Promise<User> {
|
||||
return this.prisma.user.create({
|
||||
data: {
|
||||
username: `Guest ${randomUUID()}`,
|
||||
username: displayName,
|
||||
isGuest: true,
|
||||
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
|
||||
email: null,
|
||||
|
||||
@@ -9,7 +9,7 @@ Resource ./auth.resource
|
||||
*** Test Cases ***
|
||||
LoginAsGuest
|
||||
[Documentation] Login as a guest
|
||||
&{res}= POST /auth/guest
|
||||
&{res}= POST /auth/guest {"username": "i-am-a-guest"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body access_token
|
||||
@@ -20,12 +20,13 @@ LoginAsGuest
|
||||
Integer response status 200
|
||||
Boolean response body isGuest true
|
||||
Integer response body partyPlayed 0
|
||||
String response body username "i-am-a-guest"
|
||||
|
||||
[Teardown] DELETE /auth/me
|
||||
|
||||
TwoGuests
|
||||
[Documentation] Login as a guest
|
||||
&{res}= POST /auth/guest
|
||||
&{res}= POST /auth/guest {"username": "i-am-another-guest"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body access_token
|
||||
@@ -36,8 +37,9 @@ TwoGuests
|
||||
Integer response status 200
|
||||
Boolean response body isGuest true
|
||||
Integer response body partyPlayed 0
|
||||
String response body username "i-am-another-guest"
|
||||
|
||||
&{res2}= POST /auth/guest
|
||||
&{res2}= POST /auth/guest {"username": "i-am-a-third-guest"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body access_token
|
||||
@@ -48,6 +50,7 @@ TwoGuests
|
||||
Integer response status 200
|
||||
Boolean response body isGuest true
|
||||
Integer response body partyPlayed 0
|
||||
String response body username "i-am-a-third-guest"
|
||||
|
||||
[Teardown] Run Keywords DELETE /auth/me
|
||||
... AND Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
|
||||
@@ -55,7 +58,7 @@ TwoGuests
|
||||
|
||||
GuestToNormal
|
||||
[Documentation] Login as a guest and convert to a normal account
|
||||
&{res}= POST /auth/guest
|
||||
&{res}= POST /auth/guest {"username": "i-will-be-a-real-user"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body access_token
|
||||
@@ -65,11 +68,13 @@ GuestToNormal
|
||||
Output
|
||||
Integer response status 200
|
||||
Boolean response body isGuest true
|
||||
String response body username "i-will-be-a-real-user"
|
||||
|
||||
${res}= PUT /auth/me { "username": "toto", "password": "toto", "email": "awdaw@b.c"}
|
||||
${res}= PUT /auth/me { "password": "toto", "email": "awdaw@b.c"}
|
||||
Output
|
||||
Integer response status 200
|
||||
String response body username "toto"
|
||||
Boolean response body isGuest false
|
||||
String response body username "i-will-be-a-real-user"
|
||||
|
||||
[Teardown] DELETE /auth/me
|
||||
|
||||
@@ -187,12 +187,12 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
public static async createAndGetGuestAccount(): Promise<AccessToken> {
|
||||
public static async createAndGetGuestAccount(username: string): Promise<AccessToken> {
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/guest',
|
||||
method: 'POST',
|
||||
body: undefined,
|
||||
body: { username },
|
||||
},
|
||||
{ handler: AccessTokenResponseHandler }
|
||||
)
|
||||
|
||||
BIN
front/assets/google.png
Normal file
BIN
front/assets/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
40
front/components/APKDownloadButton.tsx
Normal file
40
front/components/APKDownloadButton.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ArrowCircleDown2 } from 'iconsax-react-native';
|
||||
import ButtonBase from './UI/ButtonBase';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import { Linking } from 'react-native';
|
||||
import { useState } from 'react';
|
||||
import PopupCC from './UI/PopupCC';
|
||||
|
||||
const APKDownloadButton = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
icon={ArrowCircleDown2}
|
||||
type={'filled'}
|
||||
title={translate('downloadAPK')}
|
||||
onPress={() => setIsOpen(true)}
|
||||
/>
|
||||
<PopupCC
|
||||
title={translate('downloadAPK')}
|
||||
description={translate('downloadAPKInstructions')}
|
||||
isVisible={isOpen}
|
||||
setIsVisible={setIsOpen}
|
||||
>
|
||||
<ButtonBase
|
||||
style={{}}
|
||||
icon={ArrowCircleDown2}
|
||||
type={'filled'}
|
||||
title={translate('downloadAPK')}
|
||||
onPress={() =>
|
||||
Linking.openURL('https://github.com/Chroma-Case/Chromacase/releases')
|
||||
}
|
||||
/>
|
||||
</PopupCC>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default APKDownloadButton;
|
||||
@@ -3,7 +3,7 @@ import Card, { CardBorderRadius } from './Card';
|
||||
import { VStack, Text, Image } from 'native-base';
|
||||
|
||||
type ArtistCardProps = {
|
||||
image: string;
|
||||
image?: string;
|
||||
name: string;
|
||||
id: number;
|
||||
onPress: () => void;
|
||||
@@ -18,6 +18,7 @@ const ArtistCard = (props: ArtistCardProps) => {
|
||||
<Image
|
||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }}
|
||||
source={{ uri: image }}
|
||||
fallbackSource={{ uri: require('../assets/icon.jpg') }}
|
||||
alt={name}
|
||||
/>
|
||||
<VStack>
|
||||
@@ -30,11 +31,4 @@ const ArtistCard = (props: ArtistCardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
ArtistCard.defaultProps = {
|
||||
image: 'https://picsum.photos/200',
|
||||
name: 'Artist',
|
||||
id: 0,
|
||||
onPress: () => {},
|
||||
};
|
||||
|
||||
export default ArtistCard;
|
||||
|
||||
@@ -2,9 +2,9 @@ import { HStack, IconButton, Image, Text } from 'native-base';
|
||||
import RowCustom from './RowCustom';
|
||||
import TextButton from './TextButton';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import API from '../API';
|
||||
import DurationComponent from './DurationComponent';
|
||||
import Song from '../models/Song';
|
||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||
|
||||
type FavSongRowProps = {
|
||||
song: Song;
|
||||
@@ -13,6 +13,8 @@ type FavSongRowProps = {
|
||||
};
|
||||
|
||||
const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
||||
const { mutate } = useLikeSongMutation();
|
||||
|
||||
return (
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
@@ -63,7 +65,7 @@ const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => {
|
||||
variant={'ghost'}
|
||||
borderRadius={'full'}
|
||||
onPress={() => {
|
||||
API.removeLikedSong(song.id);
|
||||
mutate({ songId: song.id, like: false });
|
||||
}}
|
||||
_icon={{
|
||||
as: MaterialIcons,
|
||||
|
||||
@@ -30,6 +30,7 @@ const GenreCard = (props: GenreCardProps) => {
|
||||
source={{
|
||||
uri: image,
|
||||
}}
|
||||
fallbackSource={{ uri: require('../assets/icon.jpg') }}
|
||||
size="md"
|
||||
/>
|
||||
</Box>
|
||||
@@ -43,10 +44,4 @@ const GenreCard = (props: GenreCardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
GenreCard.defaultProps = {
|
||||
icon: 'https://picsum.photos/200',
|
||||
name: 'Genre',
|
||||
onPress: () => {},
|
||||
};
|
||||
|
||||
export default GenreCard;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from './ElementTypes';
|
||||
import { ArrowDown2 } from 'iconsax-react-native';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import Translate from '../Translate';
|
||||
|
||||
type RawElementProps = {
|
||||
element: ElementProps;
|
||||
@@ -149,7 +150,7 @@ export const RawElement = ({ element }: RawElementProps) => {
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Text>Unknown type</Text>;
|
||||
return <Translate translationKey="unknownError" />;
|
||||
}
|
||||
})()}
|
||||
</Row>
|
||||
|
||||
135
front/components/Hoverable.ts
Normal file
135
front/components/Hoverable.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// credit to https://gist.github.com/ianmartorell/32bb7df95e5eff0a5ee2b2f55095e6a6
|
||||
// this file was repurosed from there
|
||||
// via this issue https://gist.github.com/necolas/1c494e44e23eb7f8c5864a2fac66299a
|
||||
// because RNW's pressable doesn't bubble events to parent pressables: https://github.com/necolas/react-native-web/issues/1875
|
||||
|
||||
/* eslint-disable no-inner-declarations */
|
||||
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
|
||||
|
||||
let isEnabled = false;
|
||||
|
||||
if (canUseDOM) {
|
||||
/**
|
||||
* Web browsers emulate mouse events (and hover states) after touch events.
|
||||
* This code infers when the currently-in-use modality supports hover
|
||||
* (including for multi-modality devices) and considers "hover" to be enabled
|
||||
* if a mouse movement occurs more than 1 second after the last touch event.
|
||||
* This threshold is long enough to account for longer delays between the
|
||||
* browser firing touch and mouse events on low-powered devices.
|
||||
*/
|
||||
const HOVER_THRESHOLD_MS = 1000;
|
||||
let lastTouchTimestamp = 0;
|
||||
|
||||
function enableHover() {
|
||||
if (isEnabled || Date.now() - lastTouchTimestamp < HOVER_THRESHOLD_MS) {
|
||||
return;
|
||||
}
|
||||
isEnabled = true;
|
||||
}
|
||||
|
||||
function disableHover() {
|
||||
lastTouchTimestamp = Date.now();
|
||||
if (isEnabled) {
|
||||
isEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', disableHover, true);
|
||||
document.addEventListener('touchmove', disableHover, true);
|
||||
document.addEventListener('mousemove', enableHover, true);
|
||||
}
|
||||
|
||||
function isHoverEnabled(): boolean {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
import React, { useCallback, ReactChild, useRef } from 'react';
|
||||
import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export interface HoverableProps {
|
||||
onHoverIn?: () => void;
|
||||
onHoverOut?: () => void;
|
||||
onPressIn?: () => void;
|
||||
onPressOut?: () => void;
|
||||
children: NonNullable<ReactChild>;
|
||||
}
|
||||
|
||||
export default function Hoverable({
|
||||
onHoverIn,
|
||||
onHoverOut,
|
||||
children,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
}: HoverableProps) {
|
||||
const showHover = useSharedValue(true);
|
||||
const isHovered = useSharedValue(false);
|
||||
|
||||
const hoverIn = useRef<undefined | (() => void)>(() => onHoverIn?.());
|
||||
const hoverOut = useRef<undefined | (() => void)>(() => onHoverOut?.());
|
||||
const pressIn = useRef<undefined | (() => void)>(() => onPressIn?.());
|
||||
const pressOut = useRef<undefined | (() => void)>(() => onPressOut?.());
|
||||
|
||||
hoverIn.current = onHoverIn;
|
||||
hoverOut.current = onHoverOut;
|
||||
pressIn.current = onPressIn;
|
||||
pressOut.current = onPressOut;
|
||||
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return Platform.OS === 'web' && showHover.value && isHovered.value;
|
||||
},
|
||||
(hovered, previouslyHovered) => {
|
||||
if (hovered !== previouslyHovered) {
|
||||
if (hovered && hoverIn.current) {
|
||||
// no need for runOnJS, it's always web
|
||||
hoverIn.current();
|
||||
} else if (hoverOut.current) {
|
||||
hoverOut.current();
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (isHoverEnabled() && !isHovered.value) {
|
||||
isHovered.value = true;
|
||||
}
|
||||
}, [isHovered]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (isHovered.value) {
|
||||
isHovered.value = false;
|
||||
}
|
||||
}, [isHovered]);
|
||||
|
||||
const handleGrant = useCallback(() => {
|
||||
showHover.value = false;
|
||||
pressIn.current?.();
|
||||
}, [showHover]);
|
||||
|
||||
const handleRelease = useCallback(() => {
|
||||
showHover.value = true;
|
||||
pressOut.current?.();
|
||||
}, [showHover]);
|
||||
|
||||
let webProps = {};
|
||||
if (Platform.OS === 'web') {
|
||||
webProps = {
|
||||
onMouseEnter: handleMouseEnter,
|
||||
onMouseLeave: handleMouseLeave,
|
||||
// prevent hover showing while responder
|
||||
onResponderGrant: handleGrant,
|
||||
onResponderRelease: handleRelease,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return React.cloneElement(React.Children.only(children) as any, {
|
||||
...webProps,
|
||||
// if child is Touchable
|
||||
onPressIn: handleGrant,
|
||||
onPressOut: handleRelease,
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Slider, Text, View, IconButton, Icon } from 'native-base';
|
||||
import { Slider, View, IconButton, Icon } from 'native-base';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Audio } from 'expo-av';
|
||||
import { VolumeHigh, VolumeSlash } from 'iconsax-react-native';
|
||||
import { Translate } from '../i18n/i18n';
|
||||
|
||||
export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; bpm: number }) => {
|
||||
const audio = useRef<Audio.Sound | null>(null);
|
||||
@@ -43,7 +44,7 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Text>Metronome</Text>
|
||||
<Translate translationKey="metronome" />
|
||||
<Icon as={<MaterialCommunityIcons name="metronome" size={24} color="white" />} />
|
||||
</View>
|
||||
<View
|
||||
|
||||
@@ -89,9 +89,11 @@ const PlayViewControlBar = ({
|
||||
<Text color={textColor[800]} fontSize={14} maxW={'100%'} isTruncated>
|
||||
{song.name}
|
||||
</Text>
|
||||
<Text color={textColor[900]} fontSize={12} maxW={'100%'} isTruncated>
|
||||
{song.artistId}
|
||||
</Text>
|
||||
{song.artist && (
|
||||
<Text color={textColor[900]} fontSize={12} maxW={'100%'} isTruncated>
|
||||
{song.artist?.name}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import { Box, Text, VStack, Progress, Stack } from 'native-base';
|
||||
import { Translate } from '../i18n/i18n';
|
||||
import { Box, VStack, Progress, Stack } from 'native-base';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import Card from '../components/Card';
|
||||
import UserAvatar from './UserAvatar';
|
||||
@@ -18,13 +18,14 @@ const ProgressBar = ({ xp }: { xp: number }) => {
|
||||
<Stack padding={4} space={2} direction="row" alignItems="center">
|
||||
<UserAvatar />
|
||||
<VStack alignItems={'center'} flexGrow={1} space={2}>
|
||||
<Text>{`${translate('level')} ${level}`}</Text>
|
||||
<Translate translationKey="level" format={(e) => `${e} ${level}`} />
|
||||
<Box w="100%">
|
||||
<Progress value={progessValue} mx="4" />
|
||||
</Box>
|
||||
<Text>
|
||||
{xp} / {nextLevelThreshold} {translate('levelProgress')}
|
||||
</Text>
|
||||
<Translate
|
||||
translationKey="levelProgress"
|
||||
format={(e) => `${xp} / ${nextLevelThreshold} ${e}`}
|
||||
/>
|
||||
</VStack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Heading,
|
||||
Text,
|
||||
Box,
|
||||
Card,
|
||||
Flex,
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { SearchContext } from '../views/SearchView';
|
||||
import { useQuery } from '../Queries';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import { Translate, translate } from '../i18n/i18n';
|
||||
import API from '../API';
|
||||
import LoadingComponent, { LoadingView } from './Loading';
|
||||
import ArtistCard from './ArtistCard';
|
||||
@@ -25,12 +24,13 @@ 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 ?? 'https://picsum.photos/200',
|
||||
cover: song.cover,
|
||||
});
|
||||
|
||||
const HomeSearchComponent = () => {
|
||||
@@ -84,18 +84,12 @@ type SongsSearchComponentProps = {
|
||||
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
||||
const navigation = useNavigation();
|
||||
const { songData } = React.useContext(SearchContext);
|
||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
||||
|
||||
const handleFavoriteButton = async (state: boolean, songId: number): Promise<void> => {
|
||||
if (state == false) await API.removeLikedSong(songId);
|
||||
else await API.addLikedSong(songId);
|
||||
};
|
||||
const favoritesQuery = useQuery(API.getLikedSongs(['artist']));
|
||||
const { mutate } = useLikeSongMutation();
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
||||
{translate('songsFilter')}
|
||||
</Text>
|
||||
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
<Box>
|
||||
{songData?.length ? (
|
||||
songData.slice(0, props.maxRows).map((comp, index) => (
|
||||
@@ -105,8 +99,8 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
||||
isLiked={
|
||||
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
|
||||
}
|
||||
handleLike={(state: boolean, songId: number) =>
|
||||
handleFavoriteButton(state, songId)
|
||||
handleLike={async (state: boolean, songId: number) =>
|
||||
mutate({ songId: songId, like: state })
|
||||
}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, 'song');
|
||||
@@ -115,7 +109,7 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text>{translate('errNoResults')}</Text>
|
||||
<Translate translationKey="errNoResults" />
|
||||
)}
|
||||
</Box>
|
||||
</ScrollView>
|
||||
@@ -132,9 +126,7 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
||||
{translate('artistFilter')}
|
||||
</Text>
|
||||
<Translate translationKey="artistFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
{artistData?.length ? (
|
||||
<CardGridCustom
|
||||
content={artistData
|
||||
@@ -151,7 +143,7 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
cardComponent={ArtistCard}
|
||||
/>
|
||||
) : (
|
||||
<Text>{translate('errNoResults')}</Text>
|
||||
<Translate translationKey="errNoResults" />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -163,9 +155,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
||||
{translate('genreFilter')}
|
||||
</Text>
|
||||
<Translate translationKey="genreFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
{genreData?.length ? (
|
||||
<CardGridCustom
|
||||
content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
|
||||
@@ -180,7 +170,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
||||
cardComponent={GenreCard}
|
||||
/>
|
||||
) : (
|
||||
<Text>{translate('errNoResults')}</Text>
|
||||
<Translate translationKey="errNoResults" />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -200,9 +190,7 @@ const FavoritesComponent = () => {
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<Text fontSize="xl" fontWeight="bold" mt={4}>
|
||||
{translate('songsFilter')}
|
||||
</Text>
|
||||
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
||||
<Box>
|
||||
{favoritesQuery.data?.map((songData) => (
|
||||
<FavSongRow
|
||||
@@ -268,7 +256,9 @@ const FilterSwitch = () => {
|
||||
case 'favorites':
|
||||
return <FavoritesComponent />;
|
||||
default:
|
||||
return <Text>Something very bad happened: {currentFilter}</Text>;
|
||||
return (
|
||||
<Translate translationKey="unknownError" format={(e) => `${e}: ${currentFilter}`} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ const SongRow = ({ song, onPress, handleLike, isLiked }: SongRowProps) => {
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{song.artistId ?? 'artist'}
|
||||
{song.artist?.name ?? ''}
|
||||
</Text>
|
||||
{/* <DurationInfo length={song.details.length} /> */}
|
||||
<DurationComponent length={song.details.length} />
|
||||
|
||||
@@ -195,12 +195,12 @@ const IconButton: React.FC<IconButtonProps> = ({
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: scaleFactor,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: 1,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -124,7 +124,7 @@ const InteractiveCC: React.FC<InteractiveCCProps> = ({
|
||||
Animated.timing(animatedValues[key]!, {
|
||||
toValue: stateValue,
|
||||
duration: duration,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import SignUpForm from '../../components/forms/signupform';
|
||||
import API, { APIError } from '../../API';
|
||||
import PopupCC from './PopupCC';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
import { useQuery } from '../../Queries';
|
||||
|
||||
const handleSubmit = async (username: string, password: string, email: string) => {
|
||||
try {
|
||||
@@ -36,6 +37,7 @@ const LogoutButtonCC = ({
|
||||
}: LogoutButtonCCProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const user = useQuery(API.getUserInfo);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,7 +56,7 @@ const LogoutButtonCC = ({
|
||||
isVisible={isVisible}
|
||||
setIsVisible={setIsVisible}
|
||||
>
|
||||
<SignUpForm onSubmit={handleSubmit} />
|
||||
<SignUpForm onSubmit={handleSubmit} defaultValues={{ username: user.data?.name }} />
|
||||
<ButtonBase
|
||||
style={!collapse ? { width: '100%' } : {}}
|
||||
type="outlined"
|
||||
|
||||
@@ -20,14 +20,11 @@ export interface MusicItemType {
|
||||
/** The URL for the song's cover image. */
|
||||
image: string;
|
||||
|
||||
/** The level of the song difficulty . */
|
||||
level: number;
|
||||
|
||||
/** The last score achieved for this song. */
|
||||
lastScore: number;
|
||||
lastScore: number | null | undefined;
|
||||
|
||||
/** The highest score achieved for this song. */
|
||||
bestScore: number;
|
||||
bestScore: number | null | undefined;
|
||||
|
||||
/** Indicates whether the song is liked/favorited by the user. */
|
||||
liked: boolean;
|
||||
@@ -141,9 +138,8 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
);
|
||||
|
||||
// Memoizing formatted numbers to avoid unnecessary computations.
|
||||
const formattedLevel = useMemo(() => formatNumber(props.level), [props.level]);
|
||||
const formattedLastScore = useMemo(() => formatNumber(props.lastScore), [props.lastScore]);
|
||||
const formattedBestScore = useMemo(() => formatNumber(props.bestScore), [props.bestScore]);
|
||||
const formattedLastScore = useMemo(() => formatNumber(props.lastScore ?? 0), [props.lastScore]);
|
||||
const formattedBestScore = useMemo(() => formatNumber(props.bestScore ?? 0), [props.bestScore]);
|
||||
|
||||
return (
|
||||
<HStack space={screenSize === 'xl' ? 2 : 1} style={[styles.container, props.style]}>
|
||||
@@ -179,7 +175,7 @@ function MusicItemComponent(props: MusicItemType) {
|
||||
/>
|
||||
</Row>
|
||||
</Column>
|
||||
{[formattedLevel, formattedLastScore, formattedBestScore].map((value, index) => (
|
||||
{[formattedLastScore, formattedBestScore].map((value, index) => (
|
||||
<Text key={index} style={styles.stats}>
|
||||
{value}
|
||||
</Text>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FlatList, HStack, View, useBreakpointValue, useTheme, Text, Row } from
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import MusicItem, { MusicItemType } from './MusicItem';
|
||||
import ButtonBase from './ButtonBase';
|
||||
import { ArrowDown2, Chart2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
|
||||
import { ArrowDown2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
|
||||
// Props type definition for MusicItemTitle.
|
||||
@@ -176,7 +176,6 @@ function MusicListComponent({
|
||||
{translate('musicListTitleSong')}
|
||||
</Text>
|
||||
{[
|
||||
{ text: translate('musicListTitleLevel'), icon: Chart2 },
|
||||
{ text: translate('musicListTitleLastScore'), icon: ArrowRotateLeft },
|
||||
{ text: translate('musicListTitleBestScore'), icon: Cup },
|
||||
].map((value) => (
|
||||
@@ -237,6 +236,10 @@ const styles = StyleSheet.create({
|
||||
|
||||
// Using `memo` to optimize rendering performance by memorizing the component's output.
|
||||
// This ensures that the component only re-renders when its props change.
|
||||
const MusicList = memo(MusicListComponent);
|
||||
const MusicList = memo(MusicListComponent, (prev, next) => {
|
||||
console.log('AAAAA');
|
||||
console.log(prev.initialMusics, next.initialMusics);
|
||||
return prev.initialMusics.length == next.initialMusics.length;
|
||||
});
|
||||
|
||||
export default MusicList;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Stack, View, Text, Wrap, Image, Row, Column, ScrollView, useToast } from 'native-base';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { Linking, useWindowDimensions } from 'react-native';
|
||||
import { Stack, View, Text, Wrap, Image, Row, Column, ScrollView } from 'native-base';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { Linking, Platform, useWindowDimensions } from 'react-native';
|
||||
import ButtonBase from './ButtonBase';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { Translate, translate } from '../../i18n/i18n';
|
||||
import API, { APIError } from '../../API';
|
||||
import SeparatorBase from './SeparatorBase';
|
||||
import LinkBase from './LinkBase';
|
||||
@@ -11,9 +11,15 @@ import { useDispatch } from '../../state/Store';
|
||||
import { setAccessToken } from '../../state/UserSlice';
|
||||
import useColorScheme from '../../hooks/colorScheme';
|
||||
import { useAssets } from 'expo-asset';
|
||||
import APKDownloadButton from '../APKDownloadButton';
|
||||
import PopupCC from './PopupCC';
|
||||
import GuestForm from '../forms/guestForm';
|
||||
|
||||
const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promise<string> => {
|
||||
const apiAccess = await API.createAndGetGuestAccount();
|
||||
const handleGuestLogin = async (
|
||||
username: string,
|
||||
apiSetter: (accessToken: string) => void
|
||||
): Promise<string> => {
|
||||
const apiAccess = await API.createAndGetGuestAccount(username);
|
||||
apiSetter(apiAccess);
|
||||
return translate('loggedIn');
|
||||
};
|
||||
@@ -35,8 +41,8 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
}) => {
|
||||
const layout = useWindowDimensions();
|
||||
const dispatch = useDispatch();
|
||||
const toast = useToast();
|
||||
const colorScheme = useColorScheme();
|
||||
const [guestModalIsOpen, openGuestModal] = useState(false);
|
||||
const [logo] = useAssets(
|
||||
colorScheme == 'light'
|
||||
? require('../../assets/icon_light.png')
|
||||
@@ -44,6 +50,8 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const [banner] = useAssets(require('../../assets/banner.jpg'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const [googleLogo] = useAssets(require('../../assets/google.png'));
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -81,22 +89,33 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
)}
|
||||
</Row>
|
||||
<ButtonBase
|
||||
title="guest mode"
|
||||
onPress={async () => {
|
||||
try {
|
||||
handleGuestLogin((accessToken: string) => {
|
||||
dispatch(setAccessToken(accessToken));
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof APIError) {
|
||||
toast.show({ description: translate(error.userMessage) });
|
||||
return;
|
||||
}
|
||||
toast.show({ description: error as string });
|
||||
}
|
||||
}}
|
||||
title={translate('guestMode')}
|
||||
onPress={() => openGuestModal(true)}
|
||||
/>
|
||||
</Wrap>
|
||||
<PopupCC
|
||||
title={translate('guestMode')}
|
||||
isVisible={guestModalIsOpen}
|
||||
setIsVisible={openGuestModal}
|
||||
>
|
||||
<GuestForm
|
||||
onSubmit={(username) =>
|
||||
handleGuestLogin(username, (accessToken: string) => {
|
||||
dispatch(setAccessToken(accessToken));
|
||||
})
|
||||
.then(() => {
|
||||
openGuestModal(false);
|
||||
return translate('loggedIn');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof APIError) {
|
||||
return translate('usernameTaken');
|
||||
}
|
||||
return error as string;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</PopupCC>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
@@ -145,11 +164,13 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
<ButtonBase
|
||||
style={{ width: '100%' }}
|
||||
type="outlined"
|
||||
iconImage="https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png"
|
||||
iconImage={googleLogo?.at(0)?.uri}
|
||||
title={translate('continuewithgoogle')}
|
||||
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
|
||||
/>
|
||||
<SeparatorBase>or</SeparatorBase>
|
||||
<SeparatorBase>
|
||||
<Translate translationKey="or" />
|
||||
</SeparatorBase>
|
||||
<Stack
|
||||
space={3}
|
||||
justifyContent="center"
|
||||
@@ -164,6 +185,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
<Text>{link.label}</Text>
|
||||
<LinkBase text={link.text} onPress={link.onPress} />
|
||||
</Wrap>
|
||||
{Platform.OS === 'web' && <APKDownloadButton />}
|
||||
</Stack>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -6,7 +6,7 @@ import API from '../../API';
|
||||
import ButtonBase from './ButtonBase';
|
||||
import { Icon } from 'iconsax-react-native';
|
||||
import { LoadingView } from '../Loading';
|
||||
import { TranslationKey, translate } from '../../i18n/i18n';
|
||||
import { Translate, TranslationKey, translate } from '../../i18n/i18n';
|
||||
import { useNavigation } from '../../Navigation';
|
||||
import Spacer from './Spacer';
|
||||
import User from '../../models/User';
|
||||
@@ -55,7 +55,10 @@ const SongHistory = (props: { quantity: number }) => {
|
||||
return (
|
||||
<View>
|
||||
{musics.length === 0 ? (
|
||||
<Text style={{ paddingHorizontal: 16 }}>{translate('menuNoSongsPlayedYet')}</Text>
|
||||
<Translate
|
||||
style={{ paddingHorizontal: 16 }}
|
||||
translationKey="menuNoSongsPlayedYet"
|
||||
/>
|
||||
) : (
|
||||
musics.map((song) => (
|
||||
<View
|
||||
@@ -117,7 +120,7 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
|
||||
/>
|
||||
{!isSmallScreen && (
|
||||
<Text fontSize={'xl'} selectable={false}>
|
||||
Chromacase
|
||||
ChromaCase
|
||||
</Text>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -3,62 +3,42 @@ import { View } from 'react-native';
|
||||
import { useBreakpointValue } from 'native-base';
|
||||
import HomeMainSongCard from './HomeMainSongCard';
|
||||
import GoldenRatioPanel from './GoldenRatioPanel';
|
||||
import Song from '../../models/Song';
|
||||
import { useNavigation } from '../../Navigation';
|
||||
|
||||
type HomeCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
fontSize: number;
|
||||
onPress?: () => void;
|
||||
type GoldenRatioProps = {
|
||||
songs: Song[];
|
||||
};
|
||||
|
||||
const cards = [
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688155292180560/image_homeview1.png',
|
||||
title: 'Beethoven',
|
||||
artist: 'Synphony No. 9',
|
||||
fontSize: 46,
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154923090093/image_homeview2.png',
|
||||
title: 'Mozart',
|
||||
artist: 'Lieder Kantate KV 619',
|
||||
fontSize: 36,
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154499457096/image_homeview3.png',
|
||||
title: 'Back',
|
||||
artist: 'Truc Truc',
|
||||
fontSize: 26,
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154109394985/image_homeview4.png',
|
||||
title: 'Mozart',
|
||||
artist: 'Machin Machin',
|
||||
fontSize: 22,
|
||||
},
|
||||
] as [HomeCardProps, HomeCardProps, HomeCardProps, HomeCardProps];
|
||||
|
||||
const GoldenRatio = () => {
|
||||
const GoldenRatio = (props: GoldenRatioProps) => {
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const navigation = useNavigation();
|
||||
const fontSizes = [46, 36, 26, 22];
|
||||
const cards = props.songs.map((s, i) => ({
|
||||
image: s.cover,
|
||||
title: s.name,
|
||||
artist: s.artist?.name ?? '',
|
||||
fontSize: fontSizes.at(i) ?? fontSizes.at(-1)!,
|
||||
onPress: () => navigation.navigate('Play', { songId: s.id }),
|
||||
}));
|
||||
|
||||
return (
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'column' : 'row'}
|
||||
header={<HomeMainSongCard {...cards[0]} />}
|
||||
header={<HomeMainSongCard {...cards[0]!} />}
|
||||
>
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'row' : 'column'}
|
||||
header={<HomeMainSongCard {...cards[1]} />}
|
||||
header={<HomeMainSongCard {...cards[1]!} />}
|
||||
>
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'column-reverse' : 'row-reverse'}
|
||||
header={<HomeMainSongCard {...cards[2]} />}
|
||||
header={<HomeMainSongCard {...cards[2]!} />}
|
||||
>
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'row-reverse' : 'column-reverse'}
|
||||
header={<HomeMainSongCard {...cards[3]} />}
|
||||
header={<HomeMainSongCard numLinesHeader={1} {...cards[3]!} />}
|
||||
>
|
||||
<View style={{ display: 'flex', width: '100%', height: '100%' }}></View>
|
||||
</GoldenRatioPanel>
|
||||
|
||||
@@ -6,6 +6,7 @@ type HomeMainSongCardProps = {
|
||||
title: string;
|
||||
artist: string;
|
||||
fontSize: number;
|
||||
numLinesHeader: number;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
@@ -66,7 +67,7 @@ const HomeMainSongCard = (props: HomeMainSongCardProps) => {
|
||||
fontSize: props.fontSize,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
numberOfLines={2}
|
||||
numberOfLines={props.numLinesHeader}
|
||||
selectable={false}
|
||||
>
|
||||
{props.title}
|
||||
@@ -94,6 +95,7 @@ const HomeMainSongCard = (props: HomeMainSongCardProps) => {
|
||||
HomeMainSongCard.defaultProps = {
|
||||
onPress: () => {},
|
||||
fontSize: 16,
|
||||
numLinesHeader: 2,
|
||||
};
|
||||
|
||||
export default HomeMainSongCard;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import Song from '../../models/Song';
|
||||
import React from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { LikeButton } from './SongCardInfoLikeBtn';
|
||||
import { Image, Platform, View } from 'react-native';
|
||||
import { Pressable, Text, PresenceTransition, Icon, useBreakpointValue } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import { useLikeSongMutation } from '../../utils/likeSongMutation';
|
||||
import Hoverable from '../Hoverable';
|
||||
|
||||
type SongCardInfoProps = {
|
||||
song: Song;
|
||||
@@ -10,35 +15,45 @@ type SongCardInfoProps = {
|
||||
onPlay: () => void;
|
||||
};
|
||||
|
||||
const Scores = [
|
||||
{
|
||||
icon: 'warning',
|
||||
score: 3,
|
||||
},
|
||||
{
|
||||
icon: 'star',
|
||||
score: -225,
|
||||
},
|
||||
{
|
||||
icon: 'trophy',
|
||||
score: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const SongCardInfo = (props: SongCardInfoProps) => {
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const [isPlayHovered, setIsPlayHovered] = React.useState(false);
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const [isSlided, setIsSlided] = React.useState(false);
|
||||
const user = useQuery(API.getUserInfo);
|
||||
const [isLiked, setIsLiked] = React.useState(false);
|
||||
const { mutate } = useLikeSongMutation();
|
||||
|
||||
const CardDims = {
|
||||
height: isPhone ? 160 : 200,
|
||||
width: isPhone ? 160 : 200,
|
||||
};
|
||||
|
||||
const Scores = [
|
||||
{
|
||||
icon: 'time',
|
||||
score: props.song.lastScore ?? '-',
|
||||
},
|
||||
{
|
||||
icon: 'trophy',
|
||||
score: props.song.bestScore ?? '-',
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!user.data) {
|
||||
return;
|
||||
}
|
||||
setIsLiked(
|
||||
props.song.likedByUsers?.some(({ userId }) => userId === user.data?.id) ?? false
|
||||
);
|
||||
}, [user.data, props.song.likedByUsers]);
|
||||
|
||||
return (
|
||||
<View
|
||||
<Pressable
|
||||
delayHoverIn={7}
|
||||
onPress={Platform.OS === 'android' ? props.onPress : undefined}
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
@@ -48,227 +63,201 @@ const SongCardInfo = (props: SongCardInfoProps) => {
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.70)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
onHoverIn={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsHovered(false);
|
||||
setIsSlided(false);
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
delayHoverIn={7}
|
||||
isHovered={isPlayHovered ? true : undefined}
|
||||
onPress={props.onPress}
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
onHoverIn={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsHovered(false);
|
||||
setIsSlided(false);
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.7)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Scores.map((score, idx) => (
|
||||
<View
|
||||
key={score.icon + idx}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
>
|
||||
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{score.score}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
animate={{
|
||||
translateY: -55,
|
||||
}}
|
||||
onTransitionComplete={() => {
|
||||
if (isHovered) {
|
||||
setIsSlided(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: props.song.cover }}
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.7)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Scores.map((score, idx) => (
|
||||
<View
|
||||
key={score.icon + idx}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
>
|
||||
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{score.score}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<PresenceTransition
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
initial={{
|
||||
translateY: 0,
|
||||
}}
|
||||
animate={{
|
||||
translateY: -55,
|
||||
}}
|
||||
onTransitionComplete={() => {
|
||||
if (isHovered) {
|
||||
setIsSlided(true);
|
||||
}
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: props.song.cover }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{props.song.name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
>
|
||||
{props.song.artist?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
{props.song.name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
name="bookmark-outline"
|
||||
size={17}
|
||||
color="#6075F9"
|
||||
/>
|
||||
>
|
||||
{props.song.artist?.name}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<LikeButton
|
||||
color="#6075F9"
|
||||
onPress={() => {
|
||||
console.log('like');
|
||||
mutate({ songId: props.song.id, like: !isLiked });
|
||||
}}
|
||||
isLiked={isLiked}
|
||||
/>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
</View>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
{Platform.OS === 'web' && (
|
||||
<PresenceTransition
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 35,
|
||||
left: CardDims.width / 2 - 20,
|
||||
}}
|
||||
visible={isSlided}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<Hoverable
|
||||
onHoverIn={() => {
|
||||
setIsPlayHovered(true);
|
||||
}}
|
||||
visible={isSlided}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
onHoverOut={() => {
|
||||
setIsPlayHovered(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onPress();
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: isPlayHovered
|
||||
? 'rgba(96, 117, 249, 0.9)'
|
||||
: 'rgba(96, 117, 249, 0.7)',
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onHoverIn={() => {
|
||||
setIsPlayHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsPlayHovered(false);
|
||||
}}
|
||||
borderRadius={100}
|
||||
marginBottom={35}
|
||||
onPress={props.onPlay}
|
||||
>
|
||||
{({ isPressed, isHovered }) => (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: (() => {
|
||||
if (isPressed) {
|
||||
return 'rgba(96, 117, 249, 1)';
|
||||
} else if (isHovered) {
|
||||
return 'rgba(96, 117, 249, 0.9)';
|
||||
} else {
|
||||
return 'rgba(96, 117, 249, 0.7)';
|
||||
}
|
||||
})(),
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="play-outline"
|
||||
color={'white'}
|
||||
size={20}
|
||||
rounded="sm"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
<Ionicons name="play-outline" color={'white'} size={20} rounded="sm" />
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
</>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Hoverable>
|
||||
</PresenceTransition>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
37
front/components/V2/SongCardInfoLikeBtn.tsx
Normal file
37
front/components/V2/SongCardInfoLikeBtn.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Platform, View } from 'react-native';
|
||||
import { IconButton } from 'native-base';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
||||
type LikeButtonProps = {
|
||||
isLiked: boolean;
|
||||
onPress?: () => void;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const LikeButton = ({ isLiked, color, onPress }: LikeButtonProps) => {
|
||||
if (Platform.OS === 'web') {
|
||||
// painful error of no onHover event control
|
||||
return (
|
||||
<View onClick={onPress}>
|
||||
<MaterialIcons
|
||||
color={color}
|
||||
name={isLiked ? 'favorite' : 'favorite-outline'}
|
||||
size={17}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconButton
|
||||
variant={'ghost'}
|
||||
borderRadius={'full'}
|
||||
size={17}
|
||||
color={color}
|
||||
onPress={onPress}
|
||||
_icon={{
|
||||
as: MaterialIcons,
|
||||
name: isLiked ? 'favorite' : 'favorite-outline',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
67
front/components/forms/guestForm.tsx
Normal file
67
front/components/forms/guestForm.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { string } from 'yup';
|
||||
import { useToast, Column } from 'native-base';
|
||||
import TextFormField from '../UI/TextFormField';
|
||||
import ButtonBase from '../UI/ButtonBase';
|
||||
import { Sms } from 'iconsax-react-native';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
interface GuestFormProps {
|
||||
onSubmit: (username: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const validationSchemas = {
|
||||
username: string().required('Username is required'),
|
||||
};
|
||||
|
||||
const GuestForm = ({ onSubmit }: GuestFormProps) => {
|
||||
const [formData, setFormData] = React.useState({
|
||||
username: {
|
||||
value: '',
|
||||
error: null as string | null,
|
||||
},
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
return (
|
||||
<Column style={{ width: layout.width * 0.5 }}>
|
||||
<TextFormField
|
||||
style={{ marginVertical: 10 }}
|
||||
isRequired
|
||||
icon={Sms}
|
||||
placeholder={translate('username')}
|
||||
value={formData.username.value}
|
||||
error={formData.username.error}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.username
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ButtonBase
|
||||
isDisabled={formData.username.error !== null || formData.username.value === ''}
|
||||
type={'filled'}
|
||||
title={translate('submitBtn')}
|
||||
style={{ marginVertical: 10 }}
|
||||
onPress={() => {
|
||||
onSubmit(formData.username.value)
|
||||
.then((e) => {
|
||||
toast.show({ description: e as string });
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.show({ description: e as string });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuestForm;
|
||||
@@ -10,13 +10,14 @@ import ButtonBase from '../UI/ButtonBase';
|
||||
import Spacer from '../UI/Spacer';
|
||||
|
||||
interface SignupFormProps {
|
||||
defaultValues: Partial<{ username: string }>;
|
||||
onSubmit: (username: string, password: string, email: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const SignUpForm = ({ onSubmit }: SignupFormProps) => {
|
||||
const SignUpForm = ({ onSubmit, defaultValues }: SignupFormProps) => {
|
||||
const [formData, setFormData] = React.useState({
|
||||
username: {
|
||||
value: '',
|
||||
value: defaultValues.username || '',
|
||||
error: null as string | null,
|
||||
},
|
||||
password: {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const en = {
|
||||
error: 'Error',
|
||||
or: 'or',
|
||||
guestMode: 'Guest Mode',
|
||||
downloadAPK: 'Download Android App',
|
||||
goBackHome: 'Go Back Home',
|
||||
anErrorOccured: 'An Error Occured',
|
||||
welcome: 'Welcome',
|
||||
@@ -226,9 +229,6 @@ export const en = {
|
||||
SettingsPreferencesTabLanguageSectionDescription: 'Set the language of your application',
|
||||
SettingsPreferencesTabDifficultySectionTitle: 'Difficulty',
|
||||
SettingsPreferencesTabDifficultySectionDescription: 'The precision of the tempo increases',
|
||||
SettingsPreferencesTabColorblindModeSectionTitle: 'Colorblind Mode',
|
||||
SettingsPreferencesTabColorblindModeSectionDescription: 'Increases contrast',
|
||||
SettingsPreferencesTabMicVolumeSectionTitle: 'Mic Volume',
|
||||
SettingsPreferencesTabMicVolumeSectionDescription:
|
||||
'Adjust the volume of your microphone according to your preference',
|
||||
// Notifications Tab
|
||||
@@ -277,7 +277,6 @@ export const en = {
|
||||
SettingsPreferencesTheme: 'Theme',
|
||||
SettingsPreferencesLanguage: 'Language',
|
||||
SettingsPreferencesDifficulty: 'Difficulty',
|
||||
SettingsPreferencesColorblindMode: 'Colorblind mode',
|
||||
SettingsPreferencesMicVolume: 'Mic volume',
|
||||
SettingsPreferencesDevice: 'Device',
|
||||
|
||||
@@ -308,10 +307,26 @@ export const en = {
|
||||
leaderBoardHeadingFull:
|
||||
'The players having the best scores, thanks to their exceptional accuracy, are highlighted here.',
|
||||
emptySelection: 'None,',
|
||||
gamesPlayed: 'Games Played',
|
||||
metronome: 'Metronome',
|
||||
loading: 'Loading... Please Wait',
|
||||
emailCheckFailed: 'Email verification failed. The token has expired or is invalid.',
|
||||
chromacasePitch:
|
||||
'Chromacase is a free and open source project that aims to provide a complete learning experience for anyone willing to learn piano.',
|
||||
whatIsChromacase: 'What is Chromacase?',
|
||||
clickHereForMoreInfo: 'Click here for more info',
|
||||
forgotPassword: 'I forgot my password',
|
||||
updateProfile: 'Update Profile',
|
||||
accountCreatedOn: 'Account Created on',
|
||||
downloadAPKInstructions:
|
||||
"Go to the latest release, unfold the 'Assets' section, and click 'android-build.apk'.",
|
||||
};
|
||||
|
||||
export const fr: typeof en = {
|
||||
error: 'Erreur',
|
||||
or: 'ou',
|
||||
downloadAPK: "Télécharger l'App Android",
|
||||
guestMode: 'Mode Invité',
|
||||
goBackHome: "Retourner à l'accueil",
|
||||
anErrorOccured: 'Une erreur est survenue',
|
||||
welcome: 'Bienvenue',
|
||||
@@ -371,7 +386,7 @@ export const fr: typeof en = {
|
||||
menuLeaderBoard: 'Classement',
|
||||
menuSettings: 'Paramètres',
|
||||
|
||||
menuRecentlyPlayed: 'Récemment jouée',
|
||||
menuRecentlyPlayed: 'Récemment joués',
|
||||
menuNoSongsPlayedYet: "Aucune chanson jouée pour l'instant",
|
||||
|
||||
//signup
|
||||
@@ -389,7 +404,7 @@ export const fr: typeof en = {
|
||||
//music
|
||||
musicTabFavorites: 'Favoris',
|
||||
musicTabRecentlyPlayed: 'Récemment joué',
|
||||
musicTabStepUp: 'Recommandation',
|
||||
musicTabStepUp: 'Recommandations',
|
||||
|
||||
//search
|
||||
allFilter: 'Tout',
|
||||
@@ -538,9 +553,6 @@ export const fr: typeof en = {
|
||||
SettingsPreferencesTabDifficultySectionTitle: 'Difficulté',
|
||||
SettingsPreferencesTabDifficultySectionDescription:
|
||||
'La précision du tempo est de plus en plus élevée',
|
||||
SettingsPreferencesTabColorblindModeSectionTitle: 'Mode daltonien',
|
||||
SettingsPreferencesTabColorblindModeSectionDescription: 'Augmente le contraste',
|
||||
SettingsPreferencesTabMicVolumeSectionTitle: 'Volume du micro',
|
||||
SettingsPreferencesTabMicVolumeSectionDescription:
|
||||
'Réglez le volume de votre micro selon vos préférences',
|
||||
// Notifications Tab
|
||||
@@ -582,8 +594,6 @@ export const fr: typeof en = {
|
||||
SettingsNotificationsPushNotifications: 'Notifications push',
|
||||
SettingsNotificationsReleaseAlert: 'Alertes de nouvelles Sorties',
|
||||
SettingsNotificationsTrainingReminder: "Rappel d'entrainement",
|
||||
|
||||
SettingsPreferencesColorblindMode: 'Mode daltonien',
|
||||
SettingsPreferencesDevice: 'Appareil',
|
||||
SettingsPreferencesDifficulty: 'Difficulté',
|
||||
SettingsPreferencesLanguage: 'Langue',
|
||||
@@ -621,10 +631,26 @@ export const fr: typeof en = {
|
||||
leaderBoardHeadingFull:
|
||||
'Les joueurs présentant les meilleurs scores, grâce à leur précision exceptionnelle, sont mis en lumière ici.',
|
||||
emptySelection: 'Aucun',
|
||||
gamesPlayed: 'Parties Jouées',
|
||||
metronome: 'Métronome',
|
||||
loading: 'Chargement en cours... Veuillez Patienter',
|
||||
emailCheckFailed: 'La vérification du mail a échouée',
|
||||
chromacasePitch:
|
||||
"ChromaCase est une solution gratuite et open-source qui cherche à fournir une expérience d'apprentissage complète pour toutes les personnes qui cherchent à apprendre le piano.",
|
||||
whatIsChromacase: "Chromacase c'est quoi?",
|
||||
clickHereForMoreInfo: "Cliquez ici pour plus d'info",
|
||||
forgotPassword: "J'ai oublié mon mot de passe",
|
||||
updateProfile: 'Changer le Profile',
|
||||
accountCreatedOn: 'Compte créé le',
|
||||
downloadAPKInstructions:
|
||||
"Descargue 'android-build.apk' en la sección 'Assets' de la última versión.",
|
||||
};
|
||||
|
||||
export const sp: typeof en = {
|
||||
error: 'Error',
|
||||
or: 'u',
|
||||
downloadAPK: 'Descarga la Aplicación de Android',
|
||||
guestMode: 'Modo Invitado',
|
||||
anErrorOccured: 'ocurrió un error',
|
||||
goBackHome: 'regresar a casa',
|
||||
welcomeMessage: 'Benvenido',
|
||||
@@ -854,9 +880,6 @@ export const sp: typeof en = {
|
||||
SettingsPreferencesTabLanguageSectionDescription: 'Establece el idioma de tu aplicación',
|
||||
SettingsPreferencesTabDifficultySectionTitle: 'Dificultad',
|
||||
SettingsPreferencesTabDifficultySectionDescription: 'La precisión del tempo aumenta',
|
||||
SettingsPreferencesTabColorblindModeSectionTitle: 'Modo para daltónicos',
|
||||
SettingsPreferencesTabColorblindModeSectionDescription: 'Aumenta el contraste',
|
||||
SettingsPreferencesTabMicVolumeSectionTitle: 'Volumen del micrófono',
|
||||
SettingsPreferencesTabMicVolumeSectionDescription:
|
||||
'Ajusta el volumen de tu micrófono según tus preferencias',
|
||||
// Notifications Tab
|
||||
@@ -900,7 +923,6 @@ export const sp: typeof en = {
|
||||
SettingsNotificationsReleaseAlert: 'Alertas de nuevas Sorties',
|
||||
SettingsNotificationsTrainingReminder: 'Recordatorio de entrenamiento',
|
||||
|
||||
SettingsPreferencesColorblindMode: 'Modo daltoniano',
|
||||
SettingsPreferencesDevice: 'Dispositivo',
|
||||
SettingsPreferencesDifficulty: 'Dificultad',
|
||||
SettingsPreferencesLanguage: 'Idioma',
|
||||
@@ -940,4 +962,17 @@ export const sp: typeof en = {
|
||||
leaderBoardHeadingFull:
|
||||
'Aquí se destacan los jugadores que tienen las mejores puntuaciones, gracias a su precisión excepcional.',
|
||||
emptySelection: 'Nada',
|
||||
gamesPlayed: 'Juegos jugados',
|
||||
metronome: 'Metrónomo',
|
||||
loading: 'Cargando por favor espere',
|
||||
emailCheckFailed: 'Error en la verificación del correo electrónico',
|
||||
chromacasePitch:
|
||||
'ChromaCase es una solución gratuita y de código abierto que busca brindar una experiencia de aprendizaje completa para cualquiera que desee aprender a tocar el piano.',
|
||||
whatIsChromacase: '¿Qué es la cromacasa?',
|
||||
clickHereForMoreInfo: 'Haga clic aquí para más información',
|
||||
forgotPassword: 'Olvidé mi contraseña',
|
||||
updateProfile: 'Cambiar el perfil',
|
||||
accountCreatedOn: 'Cuenta creada el',
|
||||
downloadAPKInstructions:
|
||||
"Télécharger 'android-build.apk' dans la section 'Assets' de la dernière release",
|
||||
};
|
||||
|
||||
@@ -6,11 +6,13 @@ import ResponseHandler from './ResponseHandler';
|
||||
import API from '../API';
|
||||
import { AlbumValidator } from './Album';
|
||||
import { GenreValidator } from './Genre';
|
||||
import { SongHistoryItemWithoutSongValidator } from './SongHistory';
|
||||
|
||||
export type SongInclude = 'artist' | 'album' | 'genre' | 'SongHistory' | 'likedByUsers';
|
||||
|
||||
export const SongValidator = yup
|
||||
.object({
|
||||
.object()
|
||||
.shape({
|
||||
name: yup.string().required(),
|
||||
midiPath: yup.string().required(),
|
||||
musicXmlPath: yup.string().required(),
|
||||
@@ -20,20 +22,42 @@ export const SongValidator = yup
|
||||
difficulties: SongDetailsValidator.required(),
|
||||
details: SongDetailsValidator.required(),
|
||||
cover: yup.string().required(),
|
||||
SongHistory: yup
|
||||
.lazy(() => yup.array(SongHistoryItemWithoutSongValidator.default(undefined)))
|
||||
.optional(),
|
||||
bestScore: yup.number().optional().nullable(),
|
||||
lastScore: yup.number().optional().nullable(),
|
||||
artist: yup.lazy(() => ArtistValidator.default(undefined)).optional(),
|
||||
album: yup.lazy(() => AlbumValidator.default(undefined)).optional(),
|
||||
genre: yup.lazy(() => GenreValidator.default(undefined)).optional(),
|
||||
likedByUsers: yup
|
||||
.lazy(() =>
|
||||
yup.array(yup.object({ userId: yup.number().required() })).default(undefined)
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.concat(ModelValidator)
|
||||
.transform((song: Song) => ({
|
||||
...song,
|
||||
cover: `${API.baseUrl}/song/${song.id}/illustration`,
|
||||
details: song.difficulties,
|
||||
bestScore:
|
||||
song.SongHistory?.map(({ info }) => info.score)
|
||||
.sort()
|
||||
.at(-1) ?? null,
|
||||
lastScore:
|
||||
song.SongHistory?.map(({ info, playDate }) => ({ info, playDate }))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
yup.date().cast(a.playDate)!.getTime() -
|
||||
yup.date().cast(b.playDate)!.getTime()
|
||||
)
|
||||
.at(0)?.info.score ?? null,
|
||||
}));
|
||||
|
||||
export type Song = yup.InferType<typeof SongValidator>;
|
||||
|
||||
export const SongHandler: ResponseHandler<Song> = {
|
||||
export const SongHandler: ResponseHandler<yup.InferType<typeof SongValidator>> = {
|
||||
validator: SongValidator,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ import ResponseHandler from './ResponseHandler';
|
||||
import { ModelValidator } from './Model';
|
||||
import { SongValidator } from './Song';
|
||||
|
||||
export const SongHistoryItemValidator = yup
|
||||
export const SongHistoryItemWithoutSongValidator = yup
|
||||
.object({
|
||||
songID: yup.number().required(),
|
||||
song: SongValidator.optional().default(undefined),
|
||||
userID: yup.number().required(),
|
||||
info: yup
|
||||
.object({
|
||||
@@ -27,6 +26,13 @@ export const SongHistoryItemValidator = yup
|
||||
difficulties: yup.mixed().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const SongHistoryItemValidator = SongHistoryItemWithoutSongValidator.concat(
|
||||
yup.object({
|
||||
song: yup.lazy(() => SongValidator.default(undefined)).optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export type SongHistoryItem = yup.InferType<typeof SongHistoryItemValidator>;
|
||||
|
||||
export const SongHistoryItemHandler: ResponseHandler<SongHistoryItem> = {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"expo-splash-screen": "~0.20.5",
|
||||
"expo-status-bar": "~1.6.0",
|
||||
"expo-system-ui": "~2.4.0",
|
||||
"fbjs": "^3.0.5",
|
||||
"i18next": "^23.5.1",
|
||||
"iconsax-react-native": "^0.0.8",
|
||||
"native-base": "^3.4.28",
|
||||
@@ -65,6 +66,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/plugin-transform-export-namespace-from": "^7.22.11",
|
||||
"@types/fbjs": "^3.0.10",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/react": "~18.2.14",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
|
||||
19
front/utils/likeSongMutation.ts
Normal file
19
front/utils/likeSongMutation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import API from '../API';
|
||||
|
||||
/**
|
||||
* Mutation to like/unlike a song
|
||||
*/
|
||||
export const useLikeSongMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(({ songId, like }: { songId: number; like: boolean }) => {
|
||||
const apiCall = like ? API.addLikedSong : API.removeLikedSong;
|
||||
|
||||
return apiCall(songId).then(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['liked songs'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['songs'] });
|
||||
queryClient.invalidateQueries({ queryKey: [songId] });
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import SongRow from '../components/SongRow';
|
||||
import { Key } from 'react';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import { ImageBackground } from 'react-native';
|
||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||
|
||||
type ArtistDetailsViewProps = {
|
||||
artistId: number;
|
||||
@@ -20,11 +21,7 @@ const ArtistDetailsView = ({ artistId }: RouteProps<ArtistDetailsViewProps>) =>
|
||||
const navigation = useNavigation();
|
||||
|
||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
||||
|
||||
const handleFavoriteButton = async (state: boolean, songId: number): Promise<void> => {
|
||||
if (state == false) await API.removeLikedSong(songId);
|
||||
else await API.addLikedSong(songId);
|
||||
};
|
||||
const { mutate } = useLikeSongMutation();
|
||||
|
||||
if (artistQuery.isError || songsQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
@@ -49,12 +46,12 @@ const ArtistDetailsView = ({ artistId }: RouteProps<ArtistDetailsViewProps>) =>
|
||||
{songsQuery.data.map((comp: Song, index: Key | null | undefined) => (
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
song={{ ...comp, artist: artistQuery.data }}
|
||||
isLiked={
|
||||
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
|
||||
}
|
||||
handleLike={(state: boolean, songId: number) =>
|
||||
handleFavoriteButton(state, songId)
|
||||
handleLike={async (state: boolean, songId: number) =>
|
||||
mutate({ songId: songId, like: state })
|
||||
}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, 'song');
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import API from '../API';
|
||||
import { setAccessToken } from '../state/UserSlice';
|
||||
import { Text } from 'native-base';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
import { AccessTokenResponseHandler } from '../models/AccessTokenResponse';
|
||||
import { Translate } from '../i18n/i18n';
|
||||
|
||||
const GoogleView = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -25,7 +25,7 @@ const GoogleView = () => {
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return <Text>Loading please wait</Text>;
|
||||
return <Translate translationKey={'loading'} />;
|
||||
};
|
||||
|
||||
export default GoogleView;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Center, Text, useBreakpointValue, useTheme } from 'native-base';
|
||||
import { Center, useBreakpointValue, useTheme } from 'native-base';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import {
|
||||
TabView,
|
||||
@@ -12,28 +12,29 @@ import {
|
||||
import { Heart, Clock, StatusUp, FolderCross } from 'iconsax-react-native';
|
||||
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import { TranslationKey, translate } from '../i18n/i18n';
|
||||
import { Translate, TranslationKey } from '../i18n/i18n';
|
||||
import ScaffoldCC from '../components/UI/ScaffoldCC';
|
||||
import MusicList from '../components/UI/MusicList';
|
||||
import { useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||
|
||||
export const FavoritesMusic = () => {
|
||||
const navigation = useNavigation();
|
||||
const likedSongs = useQuery(API.getLikedSongs(['artist']));
|
||||
const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory']));
|
||||
const { mutateAsync } = useLikeSongMutation();
|
||||
|
||||
const musics =
|
||||
likedSongs.data?.map((x) => ({
|
||||
artist: x.song.artist!.name,
|
||||
song: x.song.name,
|
||||
image: x.song.cover,
|
||||
level: 42,
|
||||
lastScore: 42,
|
||||
bestScore: 42,
|
||||
lastScore: x.song.lastScore,
|
||||
bestScore: x.song.bestScore,
|
||||
liked: true,
|
||||
onLike: () => {
|
||||
console.log('onLike');
|
||||
mutateAsync({ songId: x.song.id, like: false }).then(() => likedSongs.refetch());
|
||||
},
|
||||
onPlay: () => navigation.navigate('Play', { songId: x.song.id }),
|
||||
})) ?? [];
|
||||
@@ -99,7 +100,7 @@ export const FavoritesMusic = () => {
|
||||
export const RecentlyPlayedMusic = () => {
|
||||
return (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text>RecentlyPlayedMusic</Text>
|
||||
<Translate translationKey="recentlyPlayed" />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
@@ -107,7 +108,7 @@ export const RecentlyPlayedMusic = () => {
|
||||
export const StepUpMusic = () => {
|
||||
return (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text>StepUpMusic</Text>
|
||||
<Translate translationKey="musicTabStepUp" />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
@@ -172,9 +173,10 @@ const MusicTab = (props: RouteProps<object>) => {
|
||||
}}
|
||||
renderLabel={({ route, color }) =>
|
||||
layout.width > 800 && (
|
||||
<Text style={{ color: color, paddingLeft: 10, overflow: 'hidden' }}>
|
||||
{translate(route.title as TranslationKey)}
|
||||
</Text>
|
||||
<Translate
|
||||
translationKey={route.title as TranslationKey}
|
||||
style={{ color: color, paddingLeft: 10, overflow: 'hidden' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
tabStyle={{ flexDirection: 'row' }}
|
||||
|
||||
@@ -74,7 +74,7 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
const navigation = useNavigation();
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const song = useQuery(API.getSong(songId), { staleTime: Infinity });
|
||||
const song = useQuery(API.getSong(songId, ['artist']), { staleTime: Infinity });
|
||||
const toast = useToast();
|
||||
const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
||||
const webSocket = useRef<WebSocket>();
|
||||
@@ -380,9 +380,7 @@ const PlayView = ({ songId, route }: RouteProps<PlayViewProps>) => {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text color={statColor} fontSize={12}>
|
||||
<Translate translationKey={label} />
|
||||
</Text>
|
||||
<Translate translationKey={label} color={statColor} fontSize={12} />
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LoadingView } from '../components/Loading';
|
||||
import { useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
import ButtonBase from '../components/UI/ButtonBase';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import { Translate, translate } from '../i18n/i18n';
|
||||
import ScoreGraph from '../components/ScoreGraph';
|
||||
import ScaffoldCC from '../components/UI/ScaffoldCC';
|
||||
|
||||
@@ -81,14 +81,18 @@ const ProfileView = (props: RouteProps<{}>) => {
|
||||
{userQuery.data.name}
|
||||
</Text>
|
||||
<ButtonBase
|
||||
title="Modifier profil"
|
||||
title={translate('updateProfile')}
|
||||
type={'filled'}
|
||||
onPress={async () => navigation.navigate('Settings', {})}
|
||||
/>
|
||||
</View>
|
||||
<Text style={{ paddingBottom: 10, fontWeight: 'bold' }}>
|
||||
Account created on {userQuery.data.data.createdAt.toLocaleDateString()}
|
||||
</Text>
|
||||
<Translate
|
||||
style={{ paddingBottom: 10, fontWeight: 'bold' }}
|
||||
translationKey="accountCreatedOn"
|
||||
format={(e) =>
|
||||
`${e} ${userQuery.data.data.createdAt.toLocaleDateString()}`
|
||||
}
|
||||
/>
|
||||
<Flex
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
@@ -96,15 +100,19 @@ const ProfileView = (props: RouteProps<{}>) => {
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Text style={{ paddingRight: 20 }}>
|
||||
Your client ID is {userQuery.data.id}
|
||||
</Text>
|
||||
<Text>{userQuery.data.data.gamesPlayed} Games played</Text>
|
||||
<Translate
|
||||
translationKey="gamesPlayed"
|
||||
format={(e) => `${userQuery.data.data.gamesPlayed} ${e}`}
|
||||
/>
|
||||
</Flex>
|
||||
</Column>
|
||||
</View>
|
||||
<Row style={{ alignItems: 'center', paddingBottom: 20 }}>
|
||||
<Text style={{ paddingRight: 20 }}>{`${translate('level')} ${level}`}</Text>
|
||||
<Translate
|
||||
style={{ paddingRight: 20 }}
|
||||
translationKey="level"
|
||||
format={(e) => `${e} ${level}`}
|
||||
/>
|
||||
<Progress
|
||||
bgColor={colors.coolGray[500]}
|
||||
value={progessValue}
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Stack,
|
||||
Box,
|
||||
useToast,
|
||||
@@ -21,7 +20,7 @@ import BigActionButton from '../components/BigActionButton';
|
||||
import API, { APIError } from '../API';
|
||||
import { setAccessToken } from '../state/UserSlice';
|
||||
import { useDispatch } from '../state/Store';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import { Translate, translate } from '../i18n/i18n';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { useAssets } from 'expo-asset';
|
||||
|
||||
@@ -73,7 +72,7 @@ const StartPageView = () => {
|
||||
}
|
||||
size={isSmallScreen ? '5xl' : '6xl'}
|
||||
/>
|
||||
<Heading fontSize={isSmallScreen ? '3xl' : '5xl'}>Chromacase</Heading>
|
||||
<Heading fontSize={isSmallScreen ? '3xl' : '5xl'}>ChromaCase</Heading>
|
||||
</Row>
|
||||
</Center>
|
||||
<Stack
|
||||
@@ -154,12 +153,9 @@ const StartPageView = () => {
|
||||
}}
|
||||
>
|
||||
<Heading fontSize="4xl" style={{ textAlign: 'center' }}>
|
||||
What is Chromacase?
|
||||
<Translate translationKey="whatIsChromacase" />
|
||||
</Heading>
|
||||
<Text fontSize={'xl'}>
|
||||
Chromacase is a free and open source project that aims to provide a complete
|
||||
learning experience for anyone willing to learn piano.
|
||||
</Text>
|
||||
<Translate fontSize={'xl'} translationKey="chromacasePitch" />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
@@ -177,7 +173,7 @@ const StartPageView = () => {
|
||||
}}
|
||||
>
|
||||
<Link href="http://eip.epitech.eu/2024/chromacase" isExternal>
|
||||
Click here for more info
|
||||
<Translate translationKey="clickHereForMoreInfo" />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -195,7 +191,9 @@ const StartPageView = () => {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Link href="/forgot_password">I forgot my password</Link>
|
||||
<Link href="/forgot_password">
|
||||
<Translate translationKey="forgotPassword" />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Column>
|
||||
|
||||
@@ -10,10 +10,14 @@ import GoldenRatio from '../../components/V2/GoldenRatio';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const HomeView = (props: RouteProps<{}>) => {
|
||||
const songsQuery = useQuery(API.getSongSuggestions(['artist']));
|
||||
const suggestionsQuery = useQuery(
|
||||
API.getSongSuggestions(['artist', 'likedByUsers', 'SongHistory'])
|
||||
);
|
||||
const navigation = useNavigation();
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const topSuggestions = suggestionsQuery.data?.slice(0, 4) ?? [];
|
||||
const suggestions = suggestionsQuery.data?.slice(4) ?? [];
|
||||
|
||||
return (
|
||||
<ScaffoldCC routeName={props.route.name}>
|
||||
@@ -33,7 +37,7 @@ const HomeView = (props: RouteProps<{}>) => {
|
||||
aspectRatio: isPhone ? 0.618 : 1.618,
|
||||
}}
|
||||
>
|
||||
<GoldenRatio />
|
||||
<GoldenRatio songs={topSuggestions} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -64,7 +68,7 @@ const HomeView = (props: RouteProps<{}>) => {
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{songsQuery.data?.map((song) => (
|
||||
{suggestions.map((song) => (
|
||||
<SongCardInfo
|
||||
key={song.id}
|
||||
song={song}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import API from '../API';
|
||||
import { Text } from 'native-base';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
import { Translate } from '../i18n/i18n';
|
||||
|
||||
const VerifiedView = () => {
|
||||
const navigation = useNavigation();
|
||||
@@ -26,9 +26,9 @@ const VerifiedView = () => {
|
||||
}, []);
|
||||
|
||||
return failed ? (
|
||||
<Text>Email verification failed. The token has expired or is invalid.</Text>
|
||||
<Translate translationKey={'emailCheckFailed'} />
|
||||
) : (
|
||||
<Text>Loading please wait</Text>
|
||||
<Translate translationKey={'loading'} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import ElementList from '../../components/GtkUI/ElementList';
|
||||
import useUserSettings from '../../hooks/userSettings';
|
||||
import { LoadingView } from '../../components/Loading';
|
||||
import { Calendar1, MonitorMobbile, Send2, Warning2 } from 'iconsax-react-native';
|
||||
|
||||
const NotificationsSettings = () => {
|
||||
const { settings, updateSettings } = useUserSettings();
|
||||
|
||||
if (!settings.data) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
return (
|
||||
<ElementList
|
||||
style={{ width: '100%' }}
|
||||
elements={[
|
||||
{
|
||||
type: 'toggle',
|
||||
icon: MonitorMobbile,
|
||||
title: translate('SettingsNotificationsTabPushNotificationsSectionTitle'),
|
||||
description: translate(
|
||||
'SettingsNotificationsTabPushNotificationsSectionDescription'
|
||||
),
|
||||
data: {
|
||||
value: settings.data.notifications.pushNotif,
|
||||
onToggle: () => {
|
||||
updateSettings({
|
||||
notifications: {
|
||||
pushNotif: !settings.data.notifications.pushNotif,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toggle',
|
||||
icon: Send2,
|
||||
title: translate('SettingsNotificationsTabEmailNotificationsSectionTitle'),
|
||||
description: translate(
|
||||
'SettingsNotificationsTabEmailNotificationsSectionDescription'
|
||||
),
|
||||
data: {
|
||||
value: settings.data.notifications.emailNotif,
|
||||
onToggle: () => {
|
||||
updateSettings({
|
||||
notifications: {
|
||||
emailNotif: !settings.data.notifications.emailNotif,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toggle',
|
||||
icon: Calendar1,
|
||||
title: translate('SettingsNotificationsTabTrainingReminderSectionTitle'),
|
||||
description: translate(
|
||||
'SettingsNotificationsTabTrainingReminderSectionDescription'
|
||||
),
|
||||
data: {
|
||||
value: settings.data.notifications.trainNotif,
|
||||
onToggle: () => {
|
||||
updateSettings({
|
||||
notifications: {
|
||||
trainNotif: !settings.data.notifications.trainNotif,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'toggle',
|
||||
icon: Warning2,
|
||||
title: translate('SettingsNotificationsTabReleaseAlertSectionTitle'),
|
||||
description: translate(
|
||||
'SettingsNotificationsTabReleaseAlertSectionDescription'
|
||||
),
|
||||
data: {
|
||||
value: settings.data.notifications.newSongNotif,
|
||||
onToggle: () => {
|
||||
updateSettings({
|
||||
notifications: {
|
||||
newSongNotif: !settings.data.notifications.newSongNotif,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsSettings;
|
||||
@@ -7,7 +7,7 @@ import { useSelector } from '../../state/Store';
|
||||
import { updateSettings } from '../../state/SettingsSlice';
|
||||
import ElementList from '../../components/GtkUI/ElementList';
|
||||
import LocalSettings from '../../models/LocalSettings';
|
||||
import { Brush2, Colorfilter, LanguageSquare, Rank, Sound } from 'iconsax-react-native';
|
||||
import { Brush2, LanguageSquare, Rank } from 'iconsax-react-native';
|
||||
|
||||
const PreferencesSettings = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -84,59 +84,6 @@ const PreferencesSettings = () => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ElementList
|
||||
elements={[
|
||||
{
|
||||
icon: Colorfilter,
|
||||
type: 'toggle',
|
||||
title: translate('SettingsPreferencesTabColorblindModeSectionTitle'),
|
||||
description: translate(
|
||||
'SettingsPreferencesTabColorblindModeSectionDescription'
|
||||
),
|
||||
data: {
|
||||
value: settings.colorBlind,
|
||||
onToggle: () => {
|
||||
dispatch(updateSettings({ colorBlind: !settings.colorBlind }));
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ElementList
|
||||
elements={[
|
||||
{
|
||||
icon: Sound,
|
||||
type: 'range',
|
||||
title: translate('SettingsPreferencesTabMicVolumeSectionTitle'),
|
||||
description: translate('SettingsPreferencesTabMicVolumeSectionDescription'),
|
||||
data: {
|
||||
value: settings.micVolume,
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: 10,
|
||||
onChange: (value) => {
|
||||
dispatch(updateSettings({ micVolume: value }));
|
||||
},
|
||||
},
|
||||
},
|
||||
/*{
|
||||
type: "dropdown",
|
||||
title: translate("SettingsPreferencesDevice"),
|
||||
data: {
|
||||
value: settings.preferedInputName || "0",
|
||||
defaultValue: "0",
|
||||
onSelect: (itemValue: string) => {
|
||||
dispatch(updateSettings({ preferedInputName: itemValue }));
|
||||
},
|
||||
options: [
|
||||
{ label: "Mic_0", value: "0" },
|
||||
{ label: "Mic_1", value: "1" },
|
||||
{ label: "Mic_2", value: "2" },
|
||||
],
|
||||
},
|
||||
},*/
|
||||
]}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import API from '../../API';
|
||||
import React from 'react';
|
||||
import { LoadingView } from '../../components/Loading';
|
||||
import ElementList from '../../components/GtkUI/ElementList';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { useQuery } from '../../Queries';
|
||||
import { Designtools, Magicpen, Star1 } from 'iconsax-react-native';
|
||||
|
||||
// Too painful to infer the settings-only, typed navigator. Gave up
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const PremiumSettings = () => {
|
||||
const userQuery = useQuery(API.getUserInfo);
|
||||
|
||||
if (!userQuery.data || userQuery.isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
const user = userQuery.data;
|
||||
return (
|
||||
<ElementList
|
||||
style={{ width: '100%' }}
|
||||
elements={[
|
||||
{
|
||||
icon: Star1,
|
||||
type: 'text',
|
||||
title: translate('settingsPremiumTabPremiumAccountSectionTitle'),
|
||||
description: translate('settingsPremiumTabPremiumAccountSectionDescription'),
|
||||
data: {
|
||||
text: translate(user.premium ? 'yes' : 'no'),
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Magicpen,
|
||||
type: 'toggle',
|
||||
title: translate('settingsPremiumTabPianoMagiqueSectionTitle'),
|
||||
description: translate('settingsPremiumTabPianoMagiqueSectionDescription'),
|
||||
helperText: translate('settingsPremiumTabPianoMagiqueSectionHelper'),
|
||||
disabled: true,
|
||||
data: {
|
||||
value: false,
|
||||
onToggle: () => {},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Designtools,
|
||||
type: 'dropdown',
|
||||
title: translate('settingsPremiumTabThemePianoSectionTitle'),
|
||||
description: translate('settingsPremiumTabThemePianoSectionDescription'),
|
||||
disabled: true,
|
||||
data: {
|
||||
value: 'default',
|
||||
onSelect: () => {},
|
||||
options: [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Catpuccino',
|
||||
value: 'catpuccino',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PremiumSettings;
|
||||
@@ -10,7 +10,8 @@ import { Google, PasswordCheck, SmsEdit, UserSquare, Verify } from 'iconsax-reac
|
||||
import ChangeEmailForm from '../../components/forms/changeEmailForm';
|
||||
import ChangePasswordForm from '../../components/forms/changePasswordForm';
|
||||
import LogoutButtonCC from '../../components/UI/LogoutButtonCC';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Platform, ScrollView } from 'react-native';
|
||||
import APKDownloadButton from '../../components/APKDownloadButton';
|
||||
|
||||
const handleChangeEmail = async (newEmail: string): Promise<string> => {
|
||||
await API.updateUserEmail(newEmail);
|
||||
@@ -162,6 +163,7 @@ const ProfileSettings = () => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{Platform.OS === 'web' && <APKDownloadButton />}
|
||||
<LogoutButtonCC isGuest={user.isGuest} buttonType={'filled'} />
|
||||
</Column>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Center, Text, useBreakpointValue, useTheme } from 'native-base';
|
||||
import { Text, useBreakpointValue, useTheme } from 'native-base';
|
||||
import ProfileSettings from './SettingsProfile';
|
||||
import NotificationsSettings from './NotificationsSettings';
|
||||
import PrivacySettings from './PrivacySettings';
|
||||
import PreferencesSettings from './PreferencesSettings';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
@@ -13,54 +12,28 @@ import {
|
||||
Route,
|
||||
SceneRendererProps,
|
||||
} from 'react-native-tab-view';
|
||||
import {
|
||||
HeartEdit,
|
||||
Star1,
|
||||
UserEdit,
|
||||
Notification,
|
||||
SecurityUser,
|
||||
Music,
|
||||
FolderCross,
|
||||
} from 'iconsax-react-native';
|
||||
import { HeartEdit, UserEdit, SecurityUser, FolderCross } from 'iconsax-react-native';
|
||||
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
|
||||
import PremiumSettings from './SettingsPremium';
|
||||
import { RouteProps } from '../../Navigation';
|
||||
import ScaffoldCC from '../../components/UI/ScaffoldCC';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
|
||||
export const PianoSettings = () => {
|
||||
return (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text>Global settings for the virtual piano</Text>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
const renderScene = SceneMap({
|
||||
profile: ProfileSettings,
|
||||
premium: PremiumSettings,
|
||||
preferences: PreferencesSettings,
|
||||
notifications: NotificationsSettings,
|
||||
privacy: PrivacySettings,
|
||||
piano: PianoSettings,
|
||||
});
|
||||
|
||||
const getTabData = (key: string) => {
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
return { index: 0, icon: UserEdit };
|
||||
case 'premium':
|
||||
return { index: 1, icon: Star1 };
|
||||
case 'preferences':
|
||||
return { index: 2, icon: HeartEdit };
|
||||
case 'notifications':
|
||||
return { index: 3, icon: Notification };
|
||||
return { index: 1, icon: HeartEdit };
|
||||
case 'privacy':
|
||||
return { index: 4, icon: SecurityUser };
|
||||
case 'piano':
|
||||
return { index: 5, icon: Music };
|
||||
return { index: 2, icon: SecurityUser };
|
||||
default:
|
||||
return { index: 6, icon: FolderCross };
|
||||
return { index: 3, icon: FolderCross };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,11 +44,8 @@ const SettingsTab = (props: RouteProps<{}>) => {
|
||||
const { colors } = useTheme();
|
||||
const routes = [
|
||||
{ key: 'profile', title: 'settingsTabProfile' },
|
||||
{ key: 'premium', title: 'settingsTabPremium' },
|
||||
{ key: 'preferences', title: 'settingsTabPreferences' },
|
||||
{ key: 'notifications', title: 'settingsTabNotifications' },
|
||||
{ key: 'privacy', title: 'settingsTabPrivacy' },
|
||||
{ key: 'piano', title: 'settingsTabPiano' },
|
||||
];
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isSmallScreen = screenSize === 'small';
|
||||
@@ -113,11 +83,8 @@ const SettingsTab = (props: RouteProps<{}>) => {
|
||||
{translate(
|
||||
route.title as
|
||||
| 'settingsTabProfile'
|
||||
| 'settingsTabPremium'
|
||||
| 'settingsTabPreferences'
|
||||
| 'settingsTabNotifications'
|
||||
| 'settingsTabPrivacy'
|
||||
| 'settingsTabPiano'
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -3169,6 +3169,13 @@
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/fbjs@^3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/fbjs/-/fbjs-3.0.10.tgz#e837db996533e28e862d8fd09b3e1c44d7f0e252"
|
||||
integrity sha512-becmqsrRvB0qwgEYy96i9cN48w8YwOeaMhpyT/sbkSuWX27LDXPESBrgSFKkgcIdk39jqcPlLdLljT2y2OcyTg==
|
||||
dependencies:
|
||||
"@types/jsdom" "*"
|
||||
|
||||
"@types/glob@^7.1.1":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
|
||||
@@ -3228,6 +3235,15 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jsdom@*":
|
||||
version "21.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.6.tgz#bcbc7b245787ea863f3da1ef19aa1dcfb9271a1b"
|
||||
integrity sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/tough-cookie" "*"
|
||||
parse5 "^7.0.0"
|
||||
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
@@ -3342,6 +3358,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
|
||||
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
|
||||
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
|
||||
|
||||
"@types/use-sync-external-store@^0.0.3":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
|
||||
@@ -5365,7 +5386,7 @@ entities@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
|
||||
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
|
||||
|
||||
entities@^4.2.0:
|
||||
entities@^4.2.0, entities@^4.4.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
@@ -6070,7 +6091,7 @@ fbjs-css-vars@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
|
||||
integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==
|
||||
|
||||
fbjs@^3.0.0, fbjs@^3.0.4:
|
||||
fbjs@^3.0.0, fbjs@^3.0.4, fbjs@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.5.tgz#aa0edb7d5caa6340011790bd9249dbef8a81128d"
|
||||
integrity sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==
|
||||
@@ -9198,6 +9219,13 @@ parse-png@^2.1.0:
|
||||
dependencies:
|
||||
pngjs "^3.3.0"
|
||||
|
||||
parse5@^7.0.0:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
|
||||
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
|
||||
dependencies:
|
||||
entities "^4.4.0"
|
||||
|
||||
parseurl@~1.3.2, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
|
||||
Reference in New Issue
Block a user