Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cec07b7e99 | |||
| f93968c3eb | |||
| f80253cea3 |
+10
-13
@@ -12,30 +12,27 @@ jobs:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
back: ${{ steps.filter.outputs.back }}
|
||||
front: ${{ steps.filter.outputs.front }}
|
||||
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
scoro: ${{ steps.filter.outputs.scoro }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
back:
|
||||
- 'back/**'
|
||||
- '.github/workflows/back.yml'
|
||||
front:
|
||||
- 'front/**'
|
||||
- '.github/workflows/front.yml'
|
||||
scorometer:
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
scoro:
|
||||
- 'scorometer/**'
|
||||
- '.github/workflows/scoro.yml'
|
||||
back_build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||
if: ${{ needs.changes.outputs.backend == 'true' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./back
|
||||
@@ -50,7 +47,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: [ back_build ]
|
||||
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
+10
-13
@@ -12,31 +12,28 @@ jobs:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
back: ${{ steps.filter.outputs.back }}
|
||||
front: ${{ steps.filter.outputs.front }}
|
||||
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
scoro: ${{ steps.filter.outputs.scoro }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
back:
|
||||
- 'back/**'
|
||||
- '.github/workflows/back.yml'
|
||||
front:
|
||||
- 'front/**'
|
||||
- '.github/workflows/front.yml'
|
||||
scorometer:
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
scoro:
|
||||
- 'scorometer/**'
|
||||
- '.github/workflows/scoro.yml'
|
||||
front_check:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./front
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
@@ -57,7 +54,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./front
|
||||
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
needs: [ front_check ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -11,28 +11,25 @@ jobs:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
back: ${{ steps.filter.outputs.back }}
|
||||
front: ${{ steps.filter.outputs.front }}
|
||||
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
scoro: ${{ steps.filter.outputs.scoro }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
back:
|
||||
- 'back/**'
|
||||
- '.github/workflows/back.yml'
|
||||
front:
|
||||
- 'front/**'
|
||||
- '.github/workflows/front.yml'
|
||||
scorometer:
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
scoro:
|
||||
- 'scorometer/**'
|
||||
- '.github/workflows/scoro.yml'
|
||||
scoro_test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.scorometer == 'true' }}
|
||||
if: ${{ needs.changes.outputs.scoro == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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 }
|
||||
)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Animated, StyleProp, StyleSheet, ViewStyle } from 'react-native';
|
||||
|
||||
type StyleObject = Record<string, any>;
|
||||
type InterpolatedStyleObject = Record<string, Animated.AnimatedInterpolation<any>>;
|
||||
|
||||
interface AnimatedBaseProps {
|
||||
style?: StyleObject;
|
||||
defaultStyle: StyleObject;
|
||||
hoverStyle: StyleObject;
|
||||
pressStyle: StyleObject;
|
||||
currentState: number;
|
||||
duration?: number;
|
||||
children?: React.ReactNode;
|
||||
styleContainer?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const AnimatedBase: React.FC<AnimatedBaseProps> = ({
|
||||
style,
|
||||
defaultStyle,
|
||||
hoverStyle,
|
||||
pressStyle,
|
||||
currentState,
|
||||
children,
|
||||
duration = 250,
|
||||
styleContainer,
|
||||
}) => {
|
||||
const animatedValues = useRef<Record<string, Animated.Value>>({}).current;
|
||||
|
||||
const extractTransformKeys = (styleObject: StyleObject) => {
|
||||
return styleObject.transform
|
||||
? styleObject.transform.map((t: any) => Object.keys(t)[0])
|
||||
: [];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const allStyleKeys = new Set([
|
||||
...Object.keys(defaultStyle),
|
||||
...Object.keys(hoverStyle),
|
||||
...Object.keys(pressStyle),
|
||||
]);
|
||||
|
||||
allStyleKeys.forEach((key) => {
|
||||
if (!animatedValues[key]) {
|
||||
animatedValues[key] = new Animated.Value(0);
|
||||
console.log('key; ', key);
|
||||
}
|
||||
});
|
||||
|
||||
const allTransformKeys = new Set([
|
||||
...extractTransformKeys(defaultStyle),
|
||||
...extractTransformKeys(hoverStyle),
|
||||
...extractTransformKeys(pressStyle),
|
||||
]);
|
||||
|
||||
allTransformKeys.forEach((key) => {
|
||||
if (!animatedValues[key]) {
|
||||
animatedValues[key] = new Animated.Value(0);
|
||||
console.log('keyxx; ', key);
|
||||
}
|
||||
});
|
||||
}, [defaultStyle, hoverStyle, pressStyle]);
|
||||
|
||||
useEffect(() => {
|
||||
animateToState(currentState);
|
||||
}, [currentState]);
|
||||
|
||||
const getTransformValue = (key: string, style: StyleObject) => {
|
||||
const transformObject = style.transform?.find((t: any) => t.hasOwnProperty(key));
|
||||
return transformObject ? transformObject[key] : 0;
|
||||
};
|
||||
|
||||
const interpolateStyle = (stateStyle: StyleObject): InterpolatedStyleObject => {
|
||||
const interpolatedStyle: InterpolatedStyleObject = {};
|
||||
const transform: any = [];
|
||||
|
||||
Object.keys(animatedValues).forEach((key) => {
|
||||
if (stateStyle.transform?.some((t: any) => t.hasOwnProperty(key))) {
|
||||
const defaultValue = getTransformValue(key, defaultStyle);
|
||||
const hoverValue = getTransformValue(key, hoverStyle);
|
||||
const pressValue = getTransformValue(key, pressStyle);
|
||||
|
||||
const interpolated = animatedValues[key]!.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [defaultValue, hoverValue, pressValue],
|
||||
});
|
||||
|
||||
transform.push({ [key]: interpolated });
|
||||
} else if (stateStyle[key]) {
|
||||
const defaultValue = defaultStyle[key] || 0;
|
||||
const hoverValue = hoverStyle[key] !== undefined ? hoverStyle[key] : defaultValue;
|
||||
const pressValue = pressStyle[key] !== undefined ? pressStyle[key] : defaultValue;
|
||||
|
||||
interpolatedStyle[key] = animatedValues[key]!.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [defaultValue, hoverValue, pressValue],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (transform.length > 0) {
|
||||
interpolatedStyle.transform = transform;
|
||||
}
|
||||
|
||||
return interpolatedStyle;
|
||||
};
|
||||
|
||||
const animateToState = (stateValue: number) => {
|
||||
Object.keys(animatedValues).forEach((key) => {
|
||||
Animated.timing(animatedValues[key]!, {
|
||||
toValue: stateValue,
|
||||
duration: duration,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
});
|
||||
};
|
||||
|
||||
const animatedStyle = StyleSheet.flatten([
|
||||
styleContainer,
|
||||
interpolateStyle(defaultStyle),
|
||||
interpolateStyle(hoverStyle),
|
||||
interpolateStyle(pressStyle),
|
||||
]);
|
||||
|
||||
return <Animated.View style={[style, animatedStyle]}>{children && children}</Animated.View>;
|
||||
};
|
||||
|
||||
export default AnimatedBase;
|
||||
@@ -30,7 +30,7 @@ type IconButtonProps = {
|
||||
/**
|
||||
* Callback function triggered when the button is pressed.
|
||||
*/
|
||||
onPress?: (state: boolean) => void | Promise<void>;
|
||||
onPress?: () => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Size of the icon.
|
||||
@@ -183,7 +183,7 @@ const IconButton: React.FC<IconButtonProps> = ({
|
||||
const toggleState = async () => {
|
||||
// Execute onPress if provided.
|
||||
if (onPress) {
|
||||
await onPress(!isActiveState);
|
||||
await onPress();
|
||||
}
|
||||
|
||||
// Toggle isActiveState.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Pressable } from 'react-native';
|
||||
|
||||
interface InteractiveBaseProps {
|
||||
handleHoverIn: () => void;
|
||||
handleHoverOut: () => void;
|
||||
handlePressIn: () => void;
|
||||
handlePressOut: () => void;
|
||||
children: React.ReactNode;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
const InteractiveBase: React.FC<InteractiveBaseProps> = ({
|
||||
handleHoverIn,
|
||||
handleHoverOut,
|
||||
handlePressIn,
|
||||
handlePressOut,
|
||||
children,
|
||||
style,
|
||||
}) => {
|
||||
return (
|
||||
<Pressable
|
||||
style={style}
|
||||
onHoverIn={handleHoverIn}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
onHoverOut={handleHoverOut}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default InteractiveBase;
|
||||
@@ -5,6 +5,19 @@ import { Animated, StyleSheet, Pressable, ViewStyle, StyleProp } from 'react-nat
|
||||
type StyleObject = Record<string, any>;
|
||||
type InterpolatedStyleObject = Record<string, Animated.AnimatedInterpolation<any>>;
|
||||
|
||||
const TRANSFORM_WHITELIST = {
|
||||
translateX: true,
|
||||
translateY: true,
|
||||
scale: true,
|
||||
scaleX: true,
|
||||
scaleY: true,
|
||||
rotate: true,
|
||||
rotateX: true,
|
||||
rotateY: true,
|
||||
rotateZ: true,
|
||||
perspective: true,
|
||||
};
|
||||
|
||||
interface InteractiveCCProps {
|
||||
defaultStyle: StyleObject;
|
||||
hoverStyle: StyleObject;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface MusicItemType {
|
||||
style?: ViewStyle | ViewStyle[];
|
||||
|
||||
/** Callback function triggered when the like button is pressed. */
|
||||
onLike: (state: boolean) => void;
|
||||
onLike: () => void;
|
||||
|
||||
/** Callback function triggered when the song is played. */
|
||||
onPlay: () => void;
|
||||
|
||||
@@ -237,6 +237,8 @@ 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, (prev, next) => {
|
||||
console.log('AAAAA');
|
||||
console.log(prev.initialMusics, next.initialMusics);
|
||||
return prev.initialMusics.length == next.initialMusics.length;
|
||||
});
|
||||
|
||||
|
||||
@@ -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 { 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';
|
||||
@@ -12,9 +12,14 @@ 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');
|
||||
};
|
||||
@@ -36,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')
|
||||
@@ -85,21 +90,32 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
</Row>
|
||||
<ButtonBase
|
||||
title={translate('guestMode')}
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
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,
|
||||
@@ -152,7 +168,9 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
|
||||
title={translate('continuewithgoogle')}
|
||||
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
|
||||
/>
|
||||
<SeparatorBase>or</SeparatorBase>
|
||||
<SeparatorBase>
|
||||
<Translate translationKey="or" />
|
||||
</SeparatorBase>
|
||||
<Stack
|
||||
space={3}
|
||||
justifyContent="center"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
const InteractionStates = {
|
||||
NORMAL: 0,
|
||||
HOVER: 1,
|
||||
PRESSED: 2,
|
||||
};
|
||||
|
||||
interface InteractionStateProps {
|
||||
onHoverIn?: () => void;
|
||||
onHoverOut?: () => void;
|
||||
onPressIn?: () => void;
|
||||
onPressOut?: () => void;
|
||||
}
|
||||
|
||||
const useInteractionState = ({ onHoverIn, onHoverOut, onPressIn, onPressOut }: InteractionStateProps = {}) => {
|
||||
const [state, setState] = useState(InteractionStates.NORMAL);
|
||||
|
||||
const handleHoverIn = useCallback(() => {
|
||||
setState(InteractionStates.HOVER);
|
||||
if (onHoverIn) onHoverIn();
|
||||
}, [onHoverIn]);
|
||||
|
||||
const handleHoverOut = useCallback(() => {
|
||||
setState(InteractionStates.NORMAL);
|
||||
if (onHoverOut) onHoverOut();
|
||||
}, [onHoverOut]);
|
||||
|
||||
const handlePressIn = useCallback(() => {
|
||||
setState(InteractionStates.PRESSED);
|
||||
if (onPressIn) onPressIn();
|
||||
}, [onPressIn]);
|
||||
|
||||
const handlePressOut = useCallback(() => {
|
||||
setState(InteractionStates.HOVER);
|
||||
if (onPressOut) onPressOut();
|
||||
}, [onPressOut]);
|
||||
|
||||
return {
|
||||
state,
|
||||
handleHoverIn,
|
||||
handleHoverOut,
|
||||
handlePressIn,
|
||||
handlePressOut,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInteractionState;
|
||||
@@ -1,77 +0,0 @@
|
||||
import { View } from 'react-native';
|
||||
import { Text, theme } from 'native-base';
|
||||
import { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { LoadingView } from '../Loading';
|
||||
import useColorScheme from '../../hooks/colorScheme';
|
||||
|
||||
type historyRowProps = {
|
||||
type: string;
|
||||
query: string;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
const HistoryRowComponent = (props: historyRowProps) => {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderColor:
|
||||
colorScheme == 'dark' ? theme.colors.coolGray[400] : theme.colors.coolGray[800],
|
||||
paddingTop: 5,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor:
|
||||
colorScheme == 'dark'
|
||||
? theme.colors.coolGray[600]
|
||||
: theme.colors.coolGray[400],
|
||||
borderRadius: 8,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 12,
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Text>{props.type}</Text>
|
||||
</View>
|
||||
<Text>{props.query}</Text>
|
||||
</View>
|
||||
<Text>{props.timestamp.toLocaleDateString(['fr-FR', 'en-GB'])}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchHistoryComponent = () => {
|
||||
const historyQuery = useQuery(API.getSearchHistory(0, 12));
|
||||
|
||||
if (historyQuery.isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ display: 'flex', gap: 10 }}>
|
||||
<Text fontSize={20}>{translate('histoHeading')}</Text>
|
||||
<Text>{translate('histoDesc')}</Text>
|
||||
{historyQuery.data?.map((data, index) => (
|
||||
<HistoryRowComponent
|
||||
key={index}
|
||||
type={data.type}
|
||||
query={data.query}
|
||||
timestamp={data.timestamp}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchHistoryComponent;
|
||||
@@ -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,6 @@
|
||||
export const en = {
|
||||
error: 'Error',
|
||||
or: 'or',
|
||||
guestMode: 'Guest Mode',
|
||||
downloadAPK: 'Download Android App',
|
||||
goBackHome: 'Go Back Home',
|
||||
@@ -87,8 +88,6 @@ export const en = {
|
||||
genreFilter: 'Genres',
|
||||
favoriteFilter: 'Favorites',
|
||||
searchBarPlaceholder: 'What are you looking for ?',
|
||||
histoHeading: 'Your last researches',
|
||||
histoDesc: 'Quickly find the tracks that you recently searched. Search and play away !',
|
||||
|
||||
// profile page
|
||||
user: 'Profile',
|
||||
@@ -325,6 +324,7 @@ export const en = {
|
||||
|
||||
export const fr: typeof en = {
|
||||
error: 'Erreur',
|
||||
or: 'ou',
|
||||
downloadAPK: "Télécharger l'App Android",
|
||||
guestMode: 'Mode Invité',
|
||||
goBackHome: "Retourner à l'accueil",
|
||||
@@ -413,9 +413,6 @@ export const fr: typeof en = {
|
||||
genreFilter: 'Genres',
|
||||
favoriteFilter: 'Favoris',
|
||||
searchBarPlaceholder: 'Que recherchez vous ?',
|
||||
histoHeading: 'Vos Dernières Recherches',
|
||||
histoDesc:
|
||||
'Retrouvez rapidement les morceaux que vous avez cherchés récemment. Continuez à explorer et à jouer !',
|
||||
|
||||
// Difficulty settings
|
||||
diffBtn: 'Difficulté',
|
||||
@@ -651,6 +648,7 @@ export const fr: typeof en = {
|
||||
|
||||
export const sp: typeof en = {
|
||||
error: 'Error',
|
||||
or: 'u',
|
||||
downloadAPK: 'Descarga la Aplicación de Android',
|
||||
guestMode: 'Modo Invitado',
|
||||
anErrorOccured: 'ocurrió un error',
|
||||
@@ -753,9 +751,6 @@ export const sp: typeof en = {
|
||||
genreFilter: 'géneros',
|
||||
favoriteFilter: 'Favorites',
|
||||
searchBarPlaceholder: 'Qué estás buscando ?',
|
||||
histoHeading: 'Tus últimas investigaciones',
|
||||
histoDesc:
|
||||
'Encuentra rápidamente las canciones que has estado buscando recientemente. ¡Sigue explorando y jugando!',
|
||||
|
||||
// Difficulty settings
|
||||
diffBtn: 'Dificultad',
|
||||
|
||||
+30
-139
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { View, useBreakpointValue, useTheme, Text } from 'native-base';
|
||||
import { StyleProp, ViewStyle, useWindowDimensions } from 'react-native';
|
||||
import { Center, useBreakpointValue, useTheme } from 'native-base';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import {
|
||||
TabView,
|
||||
SceneMap,
|
||||
@@ -19,132 +19,32 @@ import { useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
||||
import Song from '../models/Song';
|
||||
import InteractiveCC from '../components/UI/InteractiveCC';
|
||||
import ButtonBase from '../components/UI/ButtonBase';
|
||||
import InteractiveBase from '../components/UI/InteractiveBaseV2';
|
||||
import AnimatedBase from '../components/UI/AnimatedBase';
|
||||
import useInteractionState from '../components/UI/useInteractionState';
|
||||
|
||||
// import React from 'react';
|
||||
// import { Text, View } from 'react-native';
|
||||
// import InteractiveBase from './InteractiveBase';
|
||||
// import AnimatedBase from './AnimatedBase';
|
||||
// import useInteractionState from './useInteractionState';
|
||||
|
||||
interface LinkBaseProps {
|
||||
text: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<ViewStyle>;
|
||||
underlineStyle?: StyleProp<ViewStyle>;
|
||||
fontSize?: number;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
const AnimatedLink = ({ text, style, textStyle, underlineStyle, fontSize = 14 }: LinkBaseProps) => {
|
||||
const interaction = useInteractionState({
|
||||
onPressOut: () => { console.log("AnimatedLink is activate")}
|
||||
});
|
||||
const { colors } = useTheme();
|
||||
|
||||
const defaultUnderlineStyle = { height: fontSize / 8, bottom: 0 };
|
||||
const hoverUnderlineStyle = { height: fontSize * 1.5, bottom: 0 };
|
||||
const pressUnderlineStyle = { height: 0, bottom: fontSize * 1.5 };
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, alignItems: 'flex-start', position: 'relative'}}>
|
||||
<InteractiveBase {...interaction} style={style}>
|
||||
<AnimatedBase
|
||||
defaultStyle={{ fontSize: 14 }}
|
||||
hoverStyle={{ fontSize: 16 }}
|
||||
pressStyle={{ fontSize: 8 }}
|
||||
currentState={interaction.state}
|
||||
>
|
||||
<Text selectable={false} style={[textStyle]}>
|
||||
{/* {fontSize: fontSize}, */}
|
||||
{text}
|
||||
</Text>
|
||||
</AnimatedBase>
|
||||
<AnimatedBase
|
||||
style={[{
|
||||
minWidth: '100%',
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
backgroundColor: colors.primary[600],
|
||||
}, underlineStyle && {underlineStyle}]}
|
||||
defaultStyle={{ ...defaultUnderlineStyle }}
|
||||
hoverStyle={{ ...hoverUnderlineStyle }}
|
||||
pressStyle={{ ...pressUnderlineStyle }}
|
||||
currentState={interaction.state}
|
||||
/>
|
||||
</InteractiveBase>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
type MusicListCCProps = {
|
||||
data: Song[] | undefined;
|
||||
isLoading: boolean;
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
const MusicListCC = ({ data, isLoading, refetch }: MusicListCCProps) => {
|
||||
export const FavoritesMusic = () => {
|
||||
const navigation = useNavigation();
|
||||
const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory']));
|
||||
const { mutateAsync } = useLikeSongMutation();
|
||||
const user = useQuery(API.getUserInfo);
|
||||
|
||||
const musics = (data ?? []).map((song) => {
|
||||
const isLiked = song.likedByUsers?.some(({ userId }) => userId === user.data?.id) ?? false;
|
||||
|
||||
return {
|
||||
artist: song.artist!.name,
|
||||
song: song.name,
|
||||
image: song.cover,
|
||||
lastScore: song.lastScore,
|
||||
bestScore: song.bestScore,
|
||||
liked: isLiked,
|
||||
onLike: (state: boolean) => {
|
||||
mutateAsync({ songId: song.id, like: state }).then(() => refetch());
|
||||
const musics =
|
||||
likedSongs.data?.map((x) => ({
|
||||
artist: x.song.artist!.name,
|
||||
song: x.song.name,
|
||||
image: x.song.cover,
|
||||
lastScore: x.song.lastScore,
|
||||
bestScore: x.song.bestScore,
|
||||
liked: true,
|
||||
onLike: () => {
|
||||
mutateAsync({ songId: x.song.id, like: false }).then(() => likedSongs.refetch());
|
||||
},
|
||||
onPlay: () => navigation.navigate('Play', { songId: song.id }),
|
||||
};
|
||||
});
|
||||
onPlay: () => navigation.navigate('Play', { songId: x.song.id }),
|
||||
})) ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
if (likedSongs.isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return <MusicList initialMusics={musics} musicsPerPage={25} />;
|
||||
};
|
||||
|
||||
const FavoritesMusic = () => {
|
||||
const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory', 'likedByUsers']));
|
||||
const { colors } = useTheme();
|
||||
const interaction = useInteractionState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{margin: 30}}>
|
||||
<AnimatedLink text="coucou Je suis un link zosidjofsijdfosijfosifdjo" onPress={() => console.log("Je suis le lien !!!")}/>
|
||||
<InteractiveBase {...interaction} style={{ marginTop: 20 }}>
|
||||
<AnimatedBase
|
||||
defaultStyle={{
|
||||
backgroundColor: colors.primary[300],
|
||||
}}
|
||||
hoverStyle={{
|
||||
backgroundColor: colors.primary[900],
|
||||
}}
|
||||
pressStyle={{
|
||||
backgroundColor: colors.primary[100],
|
||||
}}
|
||||
currentState={interaction.state}
|
||||
>
|
||||
<Text>
|
||||
Text
|
||||
</Text>
|
||||
</AnimatedBase>
|
||||
</InteractiveBase>
|
||||
{/* <View style={{margin: 30}}>
|
||||
<InteractiveCC
|
||||
// duration={80}
|
||||
styleContainer={{
|
||||
@@ -188,37 +88,28 @@ const FavoritesMusic = () => {
|
||||
style={{ marginTop: 20 }}
|
||||
type={'filled'}
|
||||
/>
|
||||
</View>
|
||||
<MusicListCC
|
||||
data={likedSongs.data?.map((x) => x.song)}
|
||||
isLoading={likedSongs.isLoading}
|
||||
refetch={likedSongs.refetch}
|
||||
</View> */}
|
||||
<MusicList
|
||||
initialMusics={musics}
|
||||
// musicsPerPage={7}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RecentlyPlayedMusic = () => {
|
||||
const playHistory = useQuery(API.getUserPlayHistory(['artist', 'SongHistory', 'likedByUsers']));
|
||||
export const RecentlyPlayedMusic = () => {
|
||||
return (
|
||||
<MusicListCC
|
||||
data={
|
||||
playHistory.data?.filter((x) => x.song !== undefined).map((x) => x.song) as Song[]
|
||||
}
|
||||
isLoading={playHistory.isLoading}
|
||||
refetch={playHistory.refetch}
|
||||
/>
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Translate translationKey="recentlyPlayed" />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
const StepUpMusic = () => {
|
||||
const nextStep = useQuery(API.getSongSuggestions(['artist', 'SongHistory', 'likedByUsers']));
|
||||
export const StepUpMusic = () => {
|
||||
return (
|
||||
<MusicListCC
|
||||
data={nextStep.data ?? []}
|
||||
isLoading={nextStep.isLoading}
|
||||
refetch={nextStep.refetch}
|
||||
/>
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Translate translationKey="musicTabStepUp" />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,17 +2,12 @@ import React from 'react';
|
||||
import ScaffoldCC from '../../components/UI/ScaffoldCC';
|
||||
import SearchBarComponent from '../../components/V2/SearchBar';
|
||||
import { RouteProps } from '../../Navigation';
|
||||
import SearchHistory from '../../components/V2/SearchHistory';
|
||||
import { View } from 'react-native';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const SearchView = (props: RouteProps<{}>) => {
|
||||
return (
|
||||
<ScaffoldCC routeName={props.route.name}>
|
||||
<View style={{ display: 'flex', gap: 50 }}>
|
||||
<SearchBarComponent />
|
||||
<SearchHistory />
|
||||
</View>
|
||||
<SearchBarComponent />
|
||||
</ScaffoldCC>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user