2 Commits

Author SHA1 Message Date
Clément Le Bihan 0db8d49618 nothing important 2024-01-04 15:30:05 +01:00
Clément Le Bihan 4923fc72b2 reactènative-sounds 2023-12-31 17:59:56 +01:00
103 changed files with 182 additions and 256 deletions
+2 -3
View File
@@ -51,7 +51,6 @@ 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")
@@ -163,8 +162,8 @@ export class AuthController {
@HttpCode(200)
@ApiOperation({ description: "Login as a guest account" })
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
async guest(@Body() guestdto: GuestDto): Promise<JwtToken> {
const user = await this.usersService.createGuest(guestdto.username);
async guest(): Promise<JwtToken> {
const user = await this.usersService.createGuest();
await this.settingsService.createUserSetting(user.id);
return this.authService.login(user);
}
-8
View File
@@ -1,8 +0,0 @@
import { IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class GuestDto {
@ApiProperty()
@IsNotEmpty()
username: string;
}
+3 -3
View File
@@ -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 } from "crypto";
import { createHash, randomUUID } from "crypto";
import { createReadStream, existsSync } from "fs";
import fetch from "node-fetch";
@@ -46,10 +46,10 @@ export class UsersService {
});
}
async createGuest(displayName: string): Promise<User> {
async createGuest(): Promise<User> {
return this.prisma.user.create({
data: {
username: displayName,
username: `Guest ${randomUUID()}`,
isGuest: true,
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
email: null,
+5 -10
View File
@@ -9,7 +9,7 @@ Resource ./auth.resource
*** Test Cases ***
LoginAsGuest
[Documentation] Login as a guest
&{res}= POST /auth/guest {"username": "i-am-a-guest"}
&{res}= POST /auth/guest
Output
Integer response status 200
String response body access_token
@@ -20,13 +20,12 @@ 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 {"username": "i-am-another-guest"}
&{res}= POST /auth/guest
Output
Integer response status 200
String response body access_token
@@ -37,9 +36,8 @@ 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 {"username": "i-am-a-third-guest"}
&{res2}= POST /auth/guest
Output
Integer response status 200
String response body access_token
@@ -50,7 +48,6 @@ 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}"}
@@ -58,7 +55,7 @@ TwoGuests
GuestToNormal
[Documentation] Login as a guest and convert to a normal account
&{res}= POST /auth/guest {"username": "i-will-be-a-real-user"}
&{res}= POST /auth/guest
Output
Integer response status 200
String response body access_token
@@ -68,13 +65,11 @@ 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 { "password": "toto", "email": "awdaw@b.c"}
${res}= PUT /auth/me { "username": "toto", "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
View File
@@ -187,12 +187,12 @@ export default class API {
});
}
public static async createAndGetGuestAccount(username: string): Promise<AccessToken> {
public static async createAndGetGuestAccount(): Promise<AccessToken> {
return API.fetch(
{
route: '/auth/guest',
method: 'POST',
body: { username },
body: undefined,
},
{ handler: AccessTokenResponseHandler }
)
+8 -8
View File
@@ -1,12 +1,12 @@
import { useEffect, useRef, useState } from 'react';
import { Slider, View, IconButton, Icon } from 'native-base';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Audio } from 'expo-av';
// 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);
const audio = useRef<null>(null);
const [enabled, setEnabled] = useState<boolean>(false);
const volume = useRef<number>(50);
@@ -15,12 +15,12 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
return;
} else if (!audio.current) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Audio.Sound.createAsync(require('../assets/metronome.mp3')).then((a) => {
audio.current = a.sound;
});
// Audio.Sound.createAsync(require('../assets/metronome.mp3')).then((a) => {
// audio.current = a.sound;
// });
}
return () => {
audio.current?.unloadAsync();
// audio.current?.unloadAsync();
};
}, [enabled]);
useEffect(() => {
@@ -28,12 +28,12 @@ export const MetronomeControls = ({ paused = false, bpm }: { paused?: boolean; b
const int = setInterval(() => {
if (!enabled) return;
if (!audio.current) return;
audio.current?.playAsync();
// audio.current?.playAsync();
}, 60000 / bpm);
return () => clearInterval(int);
}, [bpm, paused]);
useEffect(() => {
audio.current?.setVolumeAsync(volume.current / 100);
// audio.current?.setVolumeAsync(volume.current / 100);
}, [volume.current]);
return (
<View flex={1}>
+42 -16
View File
@@ -5,9 +5,12 @@ import { useQuery } from '../../Queries';
import Animated, { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
import { CursorInfoItem } from '../../models/SongCursorInfos';
import { PianoNotes } from '../../state/SoundPlayerSlice';
import { Audio } from 'expo-av';
// import { Audio } from 'expo-av';
import { SvgContainer } from './SvgContainer';
import LoadingComponent from '../Loading';
import Sound from 'react-native-sound';
Sound.setCategory('Playback');
// note we are also using timestamp in a context
export type ParitionMagicProps = {
@@ -51,7 +54,7 @@ const PartitionMagic = ({
const [endPartitionReached, setEndPartitionReached] = React.useState(false);
const [isPartitionSvgLoaded, setIsPartitionSvgLoaded] = React.useState(false);
const partitionOffset = useSharedValue(0);
const pianoSounds = React.useRef<Record<string, Audio.Sound> | null>(null);
const pianoSounds = React.useRef<Record<string, Sound> | null>(null);
const cursorPaddingVertical = 10;
const cursorPaddingHorizontal = 3;
@@ -72,21 +75,40 @@ const PartitionMagic = ({
React.useEffect(() => {
if (!pianoSounds.current) {
Promise.all(
Object.entries(PianoNotes).map(([midiNumber, noteResource]) =>
Audio.Sound.createAsync(noteResource, {
volume: 1,
progressUpdateIntervalMillis: 100,
}).then((sound) => [midiNumber, sound.sound] as const)
)
Object.entries(PianoNotes).map(([midiNumber, noteResource]) => {
// Audio.Sound.createAsync(noteResource, {
// volume: 1,
// progressUpdateIntervalMillis: 100,
// }).then((sound) => [midiNumber, sound.sound] as const)
return new Promise((resolve, reject) => {
const sound = new Sound(noteResource, Sound.MAIN_BUNDLE, (error: any) => {
if (error) {
reject(error);
} else {
resolve([midiNumber, sound] as const);
}
});
});
})
).then((res) => {
pianoSounds.current = res.reduce(
(prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
{}
);
// pianoSounds.current = res.reduce(
// (prev, curr) => ({ ...prev, [curr[0]]: curr[1] }),
// {}
// );
pianoSounds.current = {};
(res as [string, Sound][]).forEach((curr) => {
pianoSounds.current![curr[0]] = curr[1];
});
console.log('sound loaded');
});
}
}, []);
}, [
() => {
pianoSounds?.current?.forEach((sound) => {
sound.release();
});
},
]);
const partitionDims = React.useMemo<[number, number]>(() => {
return [data?.pageWidth ?? 0, data?.pageHeight ?? 1];
}, [data]);
@@ -122,12 +144,16 @@ const PartitionMagic = ({
cursor.notes.forEach(({ note, duration }) => {
try {
const sound = pianoSounds.current![note]!;
sound.playAsync().catch(console.error);
sound.play((success) => {
if (!success) {
console.log('Sound did not play');
}
});
setTimeout(() => {
sound.stopAsync();
sound.stop();
}, duration - 10);
} catch (e) {
console.log(e);
console.log('Error key: ', note, e);
}
});
}
+1 -3
View File
@@ -9,7 +9,6 @@ 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 {
@@ -37,7 +36,6 @@ const LogoutButtonCC = ({
}: LogoutButtonCCProps) => {
const dispatch = useDispatch();
const [isVisible, setIsVisible] = useState(false);
const user = useQuery(API.getUserInfo);
return (
<>
@@ -56,7 +54,7 @@ const LogoutButtonCC = ({
isVisible={isVisible}
setIsVisible={setIsVisible}
>
<SignUpForm onSubmit={handleSubmit} defaultValues={{ username: user.data?.name }} />
<SignUpForm onSubmit={handleSubmit} />
<ButtonBase
style={!collapse ? { width: '100%' } : {}}
type="outlined"
+20 -38
View File
@@ -1,9 +1,9 @@
import { LinearGradient } from 'expo-linear-gradient';
import { Stack, View, Text, Wrap, Image, Row, Column, ScrollView } from 'native-base';
import { FunctionComponent, useState } from 'react';
import { Stack, View, Text, Wrap, Image, Row, Column, ScrollView, useToast } from 'native-base';
import { FunctionComponent } from 'react';
import { Linking, Platform, useWindowDimensions } from 'react-native';
import ButtonBase from './ButtonBase';
import { Translate, translate } from '../../i18n/i18n';
import { translate } from '../../i18n/i18n';
import API, { APIError } from '../../API';
import SeparatorBase from './SeparatorBase';
import LinkBase from './LinkBase';
@@ -12,14 +12,9 @@ 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 (
username: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount(username);
const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount();
apiSetter(apiAccess);
return translate('loggedIn');
};
@@ -41,8 +36,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')
@@ -90,32 +85,21 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
</Row>
<ButtonBase
title={translate('guestMode')}
onPress={() => openGuestModal(true)}
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 });
}
}}
/>
</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,
@@ -168,9 +152,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
title={translate('continuewithgoogle')}
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/>
<SeparatorBase>
<Translate translationKey="or" />
</SeparatorBase>
<SeparatorBase>or</SeparatorBase>
<Stack
space={3}
justifyContent="center"
-67
View File
@@ -1,67 +0,0 @@
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;
+2 -3
View File
@@ -10,14 +10,13 @@ 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, defaultValues }: SignupFormProps) => {
const SignUpForm = ({ onSubmit }: SignupFormProps) => {
const [formData, setFormData] = React.useState({
username: {
value: defaultValues.username || '',
value: '',
error: null as string | null,
},
password: {
-3
View File
@@ -1,6 +1,5 @@
export const en = {
error: 'Error',
or: 'or',
guestMode: 'Guest Mode',
downloadAPK: 'Download Android App',
goBackHome: 'Go Back Home',
@@ -324,7 +323,6 @@ 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",
@@ -648,7 +646,6 @@ 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',

Some files were not shown because too many files have changed in this diff Show More