fixing error from CI
This commit is contained in:
+1
-1
@@ -8,6 +8,6 @@ insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = tab
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
[{*.yaml,*.yml,*.nix}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:dev": "nest start --watch --preserveWatchOutput",
|
||||
"start:debug": "nest start --debug --watch --preserveWatchOutput",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
|
||||
@@ -97,7 +97,9 @@ export class AuthController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('reverify')
|
||||
async reverify(@Request() req: any): Promise<void> {
|
||||
await this.authService.sendVerifyMail(req.user);
|
||||
const user = await this.usersService.user({ id: req.user.id });
|
||||
if (!user) throw new BadRequestException("Invalid user");
|
||||
await this.authService.sendVerifyMail(user);
|
||||
}
|
||||
|
||||
@ApiBody({ type: LoginDto })
|
||||
|
||||
@@ -37,6 +37,7 @@ export class AuthService {
|
||||
|
||||
async sendVerifyMail(user: User) {
|
||||
if (process.env.IGNORE_MAILS === "true") return;
|
||||
console.log("Sending verification mail to", user.email);
|
||||
const token = await this.jwtService.signAsync(
|
||||
{
|
||||
userId: user.id,
|
||||
|
||||
+18
-1
@@ -40,13 +40,14 @@ services:
|
||||
retries: 5
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
front:
|
||||
build:
|
||||
context: ./front
|
||||
dockerfile: Dockerfile.dev
|
||||
environment:
|
||||
- SCOROMETER_URL=http://scorometer:6543/
|
||||
- NGINX_PORT=80
|
||||
- NGINX_PORT=4567
|
||||
ports:
|
||||
- "19006:19006"
|
||||
volumes:
|
||||
@@ -55,3 +56,19 @@ services:
|
||||
- "back"
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
environment:
|
||||
- API_URL=http://back:3000
|
||||
- SCOROMETER_URL=http://scorometer:6543
|
||||
- FRONT_URL=http://front:19006
|
||||
- PORT=4567
|
||||
depends_on:
|
||||
- back
|
||||
- front
|
||||
volumes:
|
||||
- "./front/assets:/assets:ro"
|
||||
- "./front/nginx.conf.template.dev:/etc/nginx/templates/default.conf.template:ro"
|
||||
ports:
|
||||
- "4567:4567"
|
||||
|
||||
+1
-5
@@ -35,11 +35,7 @@ services:
|
||||
retries: 5
|
||||
|
||||
front:
|
||||
build:
|
||||
context: ./front
|
||||
args:
|
||||
- API_URL=${API_URL}
|
||||
- SCORO_URL=${SCORO_URL}
|
||||
build: ./front
|
||||
environment:
|
||||
- API_URL=http://back:3000/
|
||||
- SCOROMETER_URL=http://scorometer:6543/
|
||||
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
node_modules/
|
||||
.expo/
|
||||
.idea/
|
||||
.vscode/
|
||||
.vscode/
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
|
||||
+2
-3
@@ -22,6 +22,5 @@ RUN yarn tsc && expo build:web
|
||||
# Serve the app
|
||||
FROM nginx:1.21-alpine
|
||||
COPY --from=build /app/web-build /usr/share/nginx/html
|
||||
COPY nginx.conf.template /etc/nginx/conf.d/default.conf.template
|
||||
|
||||
CMD envsubst '$API_URL $SCOROMETER_URL $NGINX_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'
|
||||
COPY ./assets/ /usr/share/nginx/html/assets/
|
||||
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
|
||||
|
||||
@@ -32,6 +32,7 @@ import GoogleView from './views/GoogleView';
|
||||
import VerifiedView from './views/VerifiedView';
|
||||
import SigninView from './views/SigninView';
|
||||
import SignupView from './views/SignupView';
|
||||
import TabNavigation from './components/V2/TabNavigation';
|
||||
|
||||
// Util function to hide route props in URL
|
||||
const removeMe = () => '';
|
||||
@@ -43,6 +44,11 @@ const protectedRoutes = () =>
|
||||
options: { title: translate('welcome'), headerLeft: null },
|
||||
link: '/',
|
||||
},
|
||||
HomeNew: {
|
||||
component: TabNavigation,
|
||||
options: { headerShown: false },
|
||||
link: '/V2',
|
||||
},
|
||||
Play: { component: PlayView, options: { title: translate('play') }, link: '/play/:songId' },
|
||||
Settings: {
|
||||
component: SetttingsNavigator,
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Slider, Switch, Text, View } from 'native-base';
|
||||
|
||||
export const Metronome = ({ paused = false, bpm }: { paused?: boolean; bpm: number }) => {
|
||||
const ref = useRef<HTMLAudioElement | null>(null);
|
||||
const enabled = useRef<boolean>(false);
|
||||
const volume = useRef<number>(50);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
const int = setInterval(() => {
|
||||
if (!enabled.current) return;
|
||||
if (!ref.current) ref.current = new Audio('/assets/metronome.mp3');
|
||||
ref.current.volume = volume.current / 100;
|
||||
ref.current.play();
|
||||
}, 60000 / bpm);
|
||||
return () => clearInterval(int);
|
||||
}, [bpm, paused]);
|
||||
return (
|
||||
<View>
|
||||
<Text>Metronome Settings</Text>
|
||||
<Text>Enabled:</Text>
|
||||
<Switch value={enabled.current} onToggle={() => (enabled.current = !enabled.current)} />
|
||||
<Text>Volume:</Text>
|
||||
<Slider
|
||||
maxWidth={'500px'}
|
||||
value={volume.current}
|
||||
onChange={(x) => (volume.current = x)}
|
||||
>
|
||||
<Slider.Track>
|
||||
<Slider.FilledTrack />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb />
|
||||
</Slider>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { PianoCursorPosition } from '../models/PianoGame';
|
||||
type PartitionCoordProps = {
|
||||
// The Buffer of the MusicXML file retreived from the API
|
||||
file: string;
|
||||
bpmRef: React.MutableRefObject<number>;
|
||||
onPartitionReady: () => void;
|
||||
onEndReached: () => void;
|
||||
onResume: () => void;
|
||||
@@ -18,6 +19,7 @@ const PartitionCoord = ({
|
||||
onEndReached,
|
||||
onPause,
|
||||
onResume,
|
||||
bpmRef,
|
||||
}: PartitionCoordProps) => {
|
||||
const [partitionData, setPartitionData] = React.useState<
|
||||
[[number, number], string, PianoCursorPosition[]] | null
|
||||
@@ -28,6 +30,7 @@ const PartitionCoord = ({
|
||||
{!partitionData && (
|
||||
<PartitionView
|
||||
file={file}
|
||||
bpmRef={bpmRef}
|
||||
onPartitionReady={(dims, base64data, a) => {
|
||||
setPartitionData([dims, base64data, a]);
|
||||
onPartitionReady();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-mixed-spaces-and-tabs */
|
||||
// Inspired from OSMD example project
|
||||
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { MutableRefObject, useEffect } from 'react';
|
||||
import {
|
||||
CursorType,
|
||||
Fraction,
|
||||
@@ -19,6 +19,7 @@ type PartitionViewProps = {
|
||||
base64data: string,
|
||||
cursorInfos: PianoCursorPosition[]
|
||||
) => void;
|
||||
bpmRef: MutableRefObject<number>;
|
||||
onEndReached: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
@@ -62,6 +63,7 @@ const PartitionView = (props: PartitionViewProps) => {
|
||||
_osmd.render();
|
||||
_osmd.cursor.show();
|
||||
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
|
||||
props.bpmRef.current = bpm;
|
||||
const wholeNoteLength = Math.round((60 / bpm) * 4000);
|
||||
const curPos = [];
|
||||
while (!_osmd.cursor.iterator.EndReached) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Image, View } from 'react-native';
|
||||
import { Text, Pressable, PresenceTransition } from 'native-base';
|
||||
|
||||
type HomeMainSongCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
fontSize: number;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const HomeMainSongCard = (props: HomeMainSongCardProps) => {
|
||||
// on hover darken the image and show the title and artist with fade in
|
||||
return (
|
||||
<Pressable onPress={props.onPress}>
|
||||
{({ isHovered }) => (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri: props.image,
|
||||
}}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexShrink: 1,
|
||||
}}
|
||||
/>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 36,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: props.fontSize,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
numberOfLines={2}
|
||||
selectable={false}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: props.fontSize * 0.4,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
numberOfLines={1}
|
||||
selectable={false}
|
||||
>
|
||||
{props.artist}
|
||||
</Text>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
HomeMainSongCard.defaultProps = {
|
||||
onPress: () => {},
|
||||
fontSize: 16,
|
||||
};
|
||||
|
||||
export default HomeMainSongCard;
|
||||
@@ -0,0 +1,279 @@
|
||||
import Song from '../../models/Song';
|
||||
import React from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { Pressable, Text, PresenceTransition, Icon } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
type SongCardInfoProps = {
|
||||
song: Song;
|
||||
onPress: () => void;
|
||||
onPlay: () => void;
|
||||
};
|
||||
|
||||
const CardDims = {
|
||||
height: 200,
|
||||
width: 200,
|
||||
};
|
||||
|
||||
const Scores = [
|
||||
{
|
||||
icon: 'warning',
|
||||
score: 3,
|
||||
},
|
||||
{
|
||||
icon: 'star',
|
||||
score: -225,
|
||||
},
|
||||
{
|
||||
icon: 'trophy',
|
||||
score: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const SongCardInfo = (props: SongCardInfoProps) => {
|
||||
const [isPlayHovered, setIsPlayHovered] = React.useState(false);
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const [isSlided, setIsSlided] = React.useState(false);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
// @ts-expect-error boxShadow isn't yet supported by react native
|
||||
boxShadow: '0px 4px 4px 0px rgba(0,0,0,0.25)',
|
||||
backDropFilter: 'blur(2px)',
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.70)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
delayHoverIn={7}
|
||||
isHovered={isPlayHovered ? true : undefined}
|
||||
onPress={props.onPress}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
onHoverIn={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsHovered(false);
|
||||
setIsSlided(false);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View
|
||||
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',
|
||||
// @ts-expect-error gap isn't yet supported by react native
|
||||
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}
|
||||
initial={{
|
||||
translateY: 0,
|
||||
}}
|
||||
animate={{
|
||||
translateY: -55,
|
||||
}}
|
||||
onTransitionComplete={() => {
|
||||
if (isHovered) {
|
||||
setIsSlided(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<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.artistId}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
name="bookmark-outline"
|
||||
size={17}
|
||||
color="#6075F9"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isSlided}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
</>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
SongCardInfo.defaultProps = {
|
||||
onPress: () => {},
|
||||
onPlay: () => {},
|
||||
};
|
||||
|
||||
export default SongCardInfo;
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useBreakpointValue } from 'native-base';
|
||||
import { View } from 'react-native';
|
||||
import TabNavigationDesktop from './TabNavigationDesktop';
|
||||
import TabNavigationPhone from './TabNavigationPhone';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import React, { useState } from 'react';
|
||||
import useColorScheme from '../../hooks/colorScheme';
|
||||
import HomeView from '../../views/V2/HomeView';
|
||||
|
||||
export type NaviTab = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
onLongPress?: () => void;
|
||||
isActive?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
iconName?: string;
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'Discovery',
|
||||
icon: <Ionicons name="search" size={24} color="black" />,
|
||||
iconName: 'search',
|
||||
},
|
||||
{
|
||||
id: 'profile',
|
||||
label: 'Profile',
|
||||
icon: <Ionicons name="person" size={24} color="black" />,
|
||||
iconName: 'person',
|
||||
},
|
||||
{
|
||||
id: 'music',
|
||||
label: 'Music',
|
||||
icon: <Ionicons name="musical-notes" size={24} color="black" />,
|
||||
iconName: 'musical-notes',
|
||||
},
|
||||
{
|
||||
id: 'search',
|
||||
label: 'Search',
|
||||
icon: <Ionicons name="search" size={24} color="black" />,
|
||||
iconName: 'search',
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
label: 'Notifications',
|
||||
icon: <Ionicons name="notifications" size={24} color="black" />,
|
||||
iconName: 'notifications',
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
icon: <Ionicons name="settings" size={24} color="white" />,
|
||||
iconName: 'settings',
|
||||
},
|
||||
] as NaviTab[];
|
||||
|
||||
const TabNavigation = () => {
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const [isDesktopCollapsed, setIsDesktopCollapsed] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? 'home');
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
const child = <HomeView />;
|
||||
|
||||
const appTabs = tabs.map((t) => {
|
||||
// use the same instance of a component between desktop and mobile
|
||||
return {
|
||||
...t,
|
||||
onPress: () => setActiveTab(t.id),
|
||||
icon: (
|
||||
<Ionicons
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
name={t.iconName as any}
|
||||
size={24}
|
||||
color={colorScheme === 'dark' ? 'white' : 'black'}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgb(26, 36, 74)',
|
||||
}}
|
||||
>
|
||||
{screenSize === 'small' ? (
|
||||
<TabNavigationPhone
|
||||
tabs={appTabs}
|
||||
activeTabID={activeTab}
|
||||
setActiveTabID={setActiveTab}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 'calc(100% - 5)',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
borderRadius: 12,
|
||||
margin: 5,
|
||||
// @ts-expect-error backDropFilter isn't yet supported by react native
|
||||
backDropFilter: 'blur(2px)',
|
||||
padding: 15,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</View>
|
||||
</TabNavigationPhone>
|
||||
) : (
|
||||
<TabNavigationDesktop
|
||||
tabs={appTabs}
|
||||
activeTabID={activeTab}
|
||||
setActiveTabID={setActiveTab}
|
||||
isCollapsed={isDesktopCollapsed}
|
||||
setIsCollapsed={setIsDesktopCollapsed}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 'calc(100% - 10)',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
borderRadius: 12,
|
||||
marginVertical: 10,
|
||||
marginRight: 10,
|
||||
// @ts-expect-error backDropFilter isn't yet supported by react native
|
||||
backDropFilter: 'blur(2px)',
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</View>
|
||||
</TabNavigationDesktop>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigation;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { View } from 'react-native';
|
||||
import { Pressable, Text } from 'native-base';
|
||||
import React from 'react';
|
||||
|
||||
type TabNavigationButtonProps = {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
onLongPress: () => void;
|
||||
isActive: boolean;
|
||||
isCollapsed: boolean;
|
||||
};
|
||||
|
||||
const TabNavigationButton = (props: TabNavigationButtonProps) => {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{({ isPressed, isHovered }) => (
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'stretch',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '10px',
|
||||
borderRadius: 8,
|
||||
flexGrow: 0,
|
||||
// @ts-expect-error BoxShadow is not in the types but I want it this may be a legitimate error on my part
|
||||
boxShadow: (() => {
|
||||
if (isHovered) {
|
||||
return '0px 0px 16px 0px rgba(0, 0, 0, 0.25)';
|
||||
} else if (props.isActive) {
|
||||
return '0px 0px 8px 0px rgba(0, 0, 0, 0.25)';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})(),
|
||||
backdropFilter: 'blur(2px)',
|
||||
backgroundColor: (() => {
|
||||
if (isPressed) {
|
||||
return 'rgba(0, 0, 0, 0.1)';
|
||||
} else if (isHovered) {
|
||||
return 'rgba(231, 231, 232, 0.2)';
|
||||
} else if (props.isActive) {
|
||||
return 'rgba(16, 16, 20, 0.5)';
|
||||
} else {
|
||||
return 'transparent';
|
||||
}
|
||||
})(),
|
||||
}}
|
||||
>
|
||||
{props.icon && (
|
||||
<View
|
||||
style={{
|
||||
marginRight: props.isCollapsed ? undefined : '10px',
|
||||
}}
|
||||
>
|
||||
{props.icon}
|
||||
</View>
|
||||
)}
|
||||
{!props.isCollapsed && (
|
||||
<Text numberOfLines={1} selectable={false}>
|
||||
{props.label}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
TabNavigationButton.defaultProps = {
|
||||
icon: undefined,
|
||||
onPress: () => {},
|
||||
onLongPress: () => {},
|
||||
isActive: false,
|
||||
isCollapsed: false,
|
||||
};
|
||||
|
||||
export default TabNavigationButton;
|
||||
@@ -0,0 +1,180 @@
|
||||
import { View, Image } from 'react-native';
|
||||
import { Divider, Text, Center, ScrollView } from 'native-base';
|
||||
import TabNavigationButton from './TabNavigationButton';
|
||||
import TabNavigationList from './TabNavigationList';
|
||||
import { useAssets } from 'expo-asset';
|
||||
import useColorScheme from '../../hooks/colorScheme';
|
||||
import { useQuery, useQueries } from '../../Queries';
|
||||
import { NaviTab } from './TabNavigation';
|
||||
import API from '../../API';
|
||||
import Song from '../../models/Song';
|
||||
|
||||
type TabNavigationDesktopProps = {
|
||||
tabs: NaviTab[];
|
||||
isCollapsed: boolean;
|
||||
setIsCollapsed: (isCollapsed: boolean) => void;
|
||||
activeTabID: string;
|
||||
setActiveTabID: (id: string) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const [icon] = useAssets(
|
||||
colorScheme == 'light'
|
||||
? require('../../assets/icon_light.png')
|
||||
: require('../../assets/icon_dark.png')
|
||||
);
|
||||
const playHistoryQuery = useQuery(API.getUserPlayHistory);
|
||||
const songHistory = useQueries(
|
||||
playHistoryQuery.data?.map(({ songID }) => API.getSong(songID)) ?? []
|
||||
);
|
||||
// settings is displayed separately (with logout)
|
||||
const buttons = props.tabs.filter((tab) => tab.id !== 'settings');
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Center>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
flexShrink: 0,
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: icon?.at(0)?.uri }}
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
width: '40px',
|
||||
height: 'auto',
|
||||
marginRight: '10px',
|
||||
}}
|
||||
/>
|
||||
<Text fontSize={'2xl'} selectable={false}>
|
||||
Chromacase
|
||||
</Text>
|
||||
</View>
|
||||
</Center>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '300px',
|
||||
height: 'auto',
|
||||
padding: '32px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<TabNavigationList
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
// @ts-expect-error gap is not in the types because we have an old version of react-native
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
{buttons.map((button, index) => (
|
||||
<TabNavigationButton
|
||||
key={'tab-navigation-button-' + index}
|
||||
icon={button.icon}
|
||||
label={button.label}
|
||||
isActive={button.id == props.activeTabID}
|
||||
onPress={button.onPress}
|
||||
onLongPress={button.onLongPress}
|
||||
isCollapsed={props.isCollapsed}
|
||||
/>
|
||||
))}
|
||||
</TabNavigationList>
|
||||
<TabNavigationList>
|
||||
<Divider />
|
||||
<TabNavigationList>
|
||||
<Text
|
||||
bold
|
||||
style={{
|
||||
paddingHorizontal: '16px',
|
||||
paddingVertical: '10px',
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
Recently played
|
||||
</Text>
|
||||
{songHistory.length === 0 && (
|
||||
<Text
|
||||
style={{
|
||||
paddingHorizontal: '16px',
|
||||
paddingVertical: '10px',
|
||||
}}
|
||||
>
|
||||
No songs played yet
|
||||
</Text>
|
||||
)}
|
||||
{songHistory
|
||||
.map((h) => h.data)
|
||||
.filter((data): data is Song => data !== undefined)
|
||||
.filter(
|
||||
(song, i, array) =>
|
||||
array.map((s) => s.id).findIndex((id) => id == song.id) == i
|
||||
)
|
||||
.slice(0, 4)
|
||||
.map((histoItem, index) => (
|
||||
<View
|
||||
key={'tab-navigation-other-' + index}
|
||||
style={{
|
||||
paddingHorizontal: '16px',
|
||||
paddingVertical: '10px',
|
||||
}}
|
||||
>
|
||||
<Text numberOfLines={1}>{histoItem.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</TabNavigationList>
|
||||
<Divider />
|
||||
<TabNavigationList
|
||||
style={{
|
||||
// @ts-expect-error gap is not in the types because we have an old version of react-native
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
{([props.tabs.find((t) => t.id === 'settings')] as NaviTab[]).map(
|
||||
(button, index) => (
|
||||
<TabNavigationButton
|
||||
key={'tab-navigation-setting-button-' + index}
|
||||
icon={button.icon}
|
||||
label={button.label}
|
||||
isActive={button.id == props.activeTabID}
|
||||
onPress={button.onPress}
|
||||
onLongPress={button.onLongPress}
|
||||
isCollapsed={props.isCollapsed}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</TabNavigationList>
|
||||
</TabNavigationList>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={{
|
||||
height: '100%',
|
||||
width: 'calc(100% - 300px)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigationDesktop;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { View, StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
type TabNavigationListProps = {
|
||||
children: React.ReactNode;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
const TabNavigationList = (props: TabNavigationListProps) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'column',
|
||||
// @ts-expect-error gap is not in the types because we have an old version of react-native
|
||||
gap: '8px',
|
||||
},
|
||||
props.style,
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigationList;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { View } from 'react-native';
|
||||
import { Center, ScrollView } from 'native-base';
|
||||
import TabNavigationButton from './TabNavigationButton';
|
||||
import { NaviTab } from './TabNavigation';
|
||||
|
||||
type TabNavigationPhoneProps = {
|
||||
tabs: NaviTab[];
|
||||
activeTabID: string;
|
||||
setActiveTabID: (id: string) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const TabNavigationPhone = (props: TabNavigationPhoneProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
padding: '16px',
|
||||
height: '90px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Center>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: '8px',
|
||||
justifyContent: 'space-evenly',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.5)',
|
||||
}}
|
||||
>
|
||||
{props.tabs.map((tab) => (
|
||||
<View key={'navigation-button-phone-' + tab.label}>
|
||||
<TabNavigationButton
|
||||
icon={tab.icon}
|
||||
label={tab.label}
|
||||
onPress={tab.onPress}
|
||||
onLongPress={tab.onLongPress}
|
||||
isActive={tab.id === props.activeTabID}
|
||||
isCollapsed={tab.id != props.activeTabID}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</Center>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'calc(100% - 90px)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavigationPhone;
|
||||
@@ -41,7 +41,7 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
|
||||
isSecret
|
||||
isRequired
|
||||
autoComplete="password"
|
||||
icon={(size, color) => <Lock1 size={size} color={color} variant="Bold" />}
|
||||
icon={Lock1}
|
||||
placeholder={translate('oldPassword')}
|
||||
value={formData.oldPassword.value}
|
||||
error={formData.oldPassword.error}
|
||||
@@ -60,7 +60,7 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
|
||||
isSecret
|
||||
isRequired
|
||||
autoComplete="password"
|
||||
icon={(size, color) => <Lock1 size={size} color={color} variant="Bold" />}
|
||||
icon={Lock1}
|
||||
placeholder={translate('newPassword')}
|
||||
value={formData.newPassword.value}
|
||||
error={formData.newPassword.error}
|
||||
@@ -79,7 +79,7 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
|
||||
isSecret
|
||||
isRequired
|
||||
autoComplete="password"
|
||||
icon={(size, color) => <Lock1 size={size} color={color} variant="Bold" />}
|
||||
icon={Lock1}
|
||||
placeholder={translate('confirmNewPassword')}
|
||||
value={formData.confirmNewPassword.value}
|
||||
error={formData.confirmNewPassword.error}
|
||||
|
||||
@@ -6,9 +6,15 @@ import API from '../API';
|
||||
export const UserValidator = yup
|
||||
.object({
|
||||
username: yup.string().required(),
|
||||
password: yup.string().required().nullable(),
|
||||
password: yup
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((value) => (value === '' ? null : value)),
|
||||
emailVerified: yup.boolean().required(),
|
||||
email: yup.string().required().nullable(),
|
||||
email: yup
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((value) => (value === '' ? null : value)),
|
||||
googleID: yup.string().required().nullable(),
|
||||
isGuest: yup.boolean().required(),
|
||||
partyPlayed: yup.number().required(),
|
||||
@@ -19,6 +25,7 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
|
||||
validator: UserValidator,
|
||||
transformer: (value) => ({
|
||||
...value,
|
||||
email: value.email ?? null,
|
||||
name: value.username,
|
||||
premium: false,
|
||||
data: {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
server {
|
||||
listen ${PORT};
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
index index.html;
|
||||
|
||||
location /assets {
|
||||
alias /assets;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass ${FRONT_URL}/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass ${API_URL}/;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Scheme $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_pass ${SCOROMETER_URL};
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"expo-secure-store": "~12.0.0",
|
||||
"expo-splash-screen": "~0.17.5",
|
||||
"expo-status-bar": "~1.4.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"format-duration": "^2.0.0",
|
||||
"i18next": "^21.8.16",
|
||||
"iconsax-react-native": "^0.0.8",
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare module '*.jpg';
|
||||
declare module '*.png';
|
||||
@@ -136,6 +136,12 @@ const HomeView = () => {
|
||||
size="sm"
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
/>
|
||||
<TextButton
|
||||
label={'V2'}
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
onPress={() => navigation.navigate('HomeNew')}
|
||||
/>
|
||||
</HStack>
|
||||
<Box style={{ width: '100%' }}>
|
||||
<Heading>
|
||||
|
||||
@@ -33,6 +33,7 @@ import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-
|
||||
import * as Linking from 'expo-linking';
|
||||
import url from 'url';
|
||||
import { PianoCanvasContext, PianoCanvasMsg, NoteTiming } from '../models/PianoGame';
|
||||
import { Metronome } from '../components/Metronome';
|
||||
|
||||
type PlayViewProps = {
|
||||
songId: number;
|
||||
@@ -83,6 +84,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
const toast = useToast();
|
||||
const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
||||
const webSocket = useRef<WebSocket>();
|
||||
const bpm = useRef<number>(60);
|
||||
const [paused, setPause] = useState<boolean>(true);
|
||||
const stopwatch = useStopwatch();
|
||||
const [time, setTime] = useState(0);
|
||||
@@ -348,6 +350,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
>
|
||||
<PartitionCoord
|
||||
file={musixml.data}
|
||||
bpmRef={bpm}
|
||||
onEndReached={onEnd}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
@@ -357,6 +360,8 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
{!partitionRendered && <LoadingComponent />}
|
||||
</View>
|
||||
|
||||
<Metronome paused={paused} bpm={bpm.current} />
|
||||
|
||||
<Box
|
||||
shadow={4}
|
||||
style={{
|
||||
|
||||
@@ -90,7 +90,7 @@ const StartPageView = () => {
|
||||
image={loginBanner?.at(0)?.uri}
|
||||
iconName="user"
|
||||
iconProvider={FontAwesome5}
|
||||
onPress={() => navigation.navigate('Login', {})}
|
||||
onPress={() => navigation.navigate('Login')}
|
||||
style={{
|
||||
width: isSmallScreen ? '90%' : 'clamp(100px, 33.3%, 600px)',
|
||||
height: '300px',
|
||||
@@ -130,7 +130,7 @@ const StartPageView = () => {
|
||||
subtitle="Create an account to save your progress"
|
||||
iconProvider={FontAwesome5}
|
||||
iconName="user-plus"
|
||||
onPress={() => navigation.navigate('Signup', {})}
|
||||
onPress={() => navigation.navigate('Signup')}
|
||||
style={{
|
||||
height: '150px',
|
||||
width: isSmallScreen ? '90%' : 'clamp(150px, 50%, 600px)',
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { View } from 'react-native';
|
||||
import { Text, useBreakpointValue } from 'native-base';
|
||||
import React from 'react';
|
||||
import { useQuery, useQueries } from '../../Queries';
|
||||
import HomeMainSongCard from '../../components/V2/HomeMainSongCard';
|
||||
import SongCardInfo from '../../components/V2/SongCardInfo';
|
||||
import API from '../../API';
|
||||
import { useNavigation } from '../../Navigation';
|
||||
|
||||
const bigSideRatio = 1000;
|
||||
const smallSideRatio = 618;
|
||||
|
||||
type HomeCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
fontSize: number;
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
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 HomeView = () => {
|
||||
const songsQuery = useQuery(API.getSongSuggestions);
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const navigation = useNavigation();
|
||||
const artistsQueries = useQueries(
|
||||
(songsQuery.data ?? []).map((song) => API.getArtist(song.artistId))
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!songsQuery.data) return;
|
||||
if (artistsQueries.every((query) => !query.isLoading)) return;
|
||||
|
||||
(songsQuery.data ?? [])
|
||||
.filter((song) =>
|
||||
artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)
|
||||
)
|
||||
.forEach((song, index) => {
|
||||
if (index > 3) return;
|
||||
cards[index]!.image = song.cover;
|
||||
cards[index]!.title = song.name;
|
||||
cards[index]!.artist = artistsQueries.find(
|
||||
(artistQuery) => artistQuery.data?.id === song.artistId
|
||||
)!.data!.name;
|
||||
cards[index]!.onPress = () => {
|
||||
navigation.navigate('Song', { songId: song.id });
|
||||
};
|
||||
});
|
||||
}, [artistsQueries]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: 'stretch',
|
||||
maxWidth: '1100px',
|
||||
alignItems: 'stretch',
|
||||
flexDirection: isPhone ? 'column' : 'row',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: bigSideRatio,
|
||||
}}
|
||||
>
|
||||
<HomeMainSongCard {...cards[0]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
display: 'flex',
|
||||
flexDirection: isPhone ? 'row' : 'column',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: bigSideRatio,
|
||||
}}
|
||||
>
|
||||
<HomeMainSongCard {...cards[1]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
display: 'flex',
|
||||
flexDirection: isPhone ? 'column-reverse' : 'row-reverse',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: bigSideRatio,
|
||||
}}
|
||||
>
|
||||
<HomeMainSongCard {...cards[2]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
display: 'flex',
|
||||
flexDirection: isPhone ? 'row-reverse' : 'column-reverse',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: bigSideRatio,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<HomeMainSongCard {...cards[3]} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: smallSideRatio,
|
||||
}}
|
||||
></View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
flexBasis: '15%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 16,
|
||||
marginBottom: 16,
|
||||
marginTop: 24,
|
||||
}}
|
||||
>
|
||||
{'Suggestions'}
|
||||
</Text>
|
||||
{songsQuery.isLoading && <Text>Loading...</Text>}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
// @ts-expect-error - gap is not in the typings
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{songsQuery.data?.map((song) => (
|
||||
<SongCardInfo
|
||||
key={song.id}
|
||||
song={song}
|
||||
onPress={() => {
|
||||
navigation.navigate('Song', { songId: song.id });
|
||||
}}
|
||||
onPlay={() => {
|
||||
console.log('play');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeView;
|
||||
@@ -6,7 +6,7 @@ import ElementList from '../../components/GtkUI/ElementList';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { useQuery } from '../../Queries';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { Google, PasswordCheck, SmsEdit, UserSquare } from 'iconsax-react-native';
|
||||
import { Google, PasswordCheck, SmsEdit, UserSquare, Verify } from 'iconsax-react-native';
|
||||
import ChangeEmailForm from '../../components/forms/changeEmailForm';
|
||||
import ChangePasswordForm from '../../components/forms/changePasswordForm';
|
||||
|
||||
@@ -53,6 +53,30 @@ const ProfileSettings = () => {
|
||||
text: user.googleID ? 'Linked' : 'Not linked',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <Verify size="24" color="#FFF" style={{ minWidth: 24 }} />,
|
||||
type: 'text',
|
||||
description: 'Vérifiez votre adresse e-mail', // TODO translate
|
||||
title: translate('verified'),
|
||||
data: {
|
||||
text: user.emailVerified ? 'verified' : 'not verified',
|
||||
onPress: user.emailVerified
|
||||
? undefined
|
||||
: () =>
|
||||
API.fetch({ route: '/auth/reverify', method: 'PUT' })
|
||||
.then(() =>
|
||||
Toast.show({
|
||||
description: 'Verification mail sent',
|
||||
})
|
||||
)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
Toast.show({
|
||||
description: 'Verification mail send error',
|
||||
});
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <UserSquare size="24" color="#FFF" style={{ minWidth: 24 }} />,
|
||||
type: 'text',
|
||||
|
||||
Reference in New Issue
Block a user