feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+114 -121
View File
@@ -1,230 +1,223 @@
import * as ImagePicker from "expo-image-picker";
import { Pressable, Text } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import React, { useGlobal } from "reactn";
import * as ImagePicker from 'expo-image-picker'
import { Pressable, Text } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import React, { useGlobal } from 'reactn'
import { responsiveWidth } from "../actions/responsiveSizes.js";
import Page from "../layouts/Page";
import { responsiveWidth } from '../actions/responsiveSizes.js'
import Page from '../layouts/Page'
import alert from "../components/Alert.js";
import Avatar from "../components/Avatar";
import InputRow from "../components/InputRow";
import ItemRowList from "../components/ItemRowList.js";
import alert from '../components/Alert.js'
import Avatar from '../components/Avatar'
import InputRow from '../components/InputRow'
import ItemRowList from '../components/ItemRowList.js'
import firebase, { usersRef } from "../config/firebase";
import firebase, { usersRef } from '../config/firebase'
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
import { Routes } from "../navigation/Routes.js";
import { Fonts, gutters, Palette, Style } from "../styles";
import { uploadFileToFirebase } from '../helpers/uploadToFirebase'
import { Routes } from '../navigation/Routes.js'
import { Fonts, gutters, Palette, Style } from '../styles'
import { useUserData } from "../providers/UserDataProvider.js";
import { useUserData } from '../providers/UserDataProvider.js'
export default ({ navigation }) => {
const { setIsLoading, setTooltip } = useMinuit();
const { onSignOut } = useUserData();
const { setIsLoading, setTooltip } = useMinuit()
const { onSignOut } = useUserData()
const [currentUID] = useGlobal("currentUID");
const [currentUserData] = useGlobal("currentUserData");
const [currentUID] = useGlobal('currentUID')
const [currentUserData] = useGlobal('currentUserData')
const settingsList = [
{
title: "Nom du compte",
key: "name",
value: currentUserData?.name || "",
type: "string",
title: 'Nom du compte',
key: 'name',
value: currentUserData?.name || '',
type: 'string',
},
{
title: "Adresse email",
key: "email",
value: currentUserData?.email || "",
type: "string",
title: 'Adresse email',
key: 'email',
value: currentUserData?.email || '',
type: 'string',
},
{
title: "Mot de passe",
key: "password",
value: "",
type: "string",
title: 'Mot de passe',
key: 'password',
value: '',
type: 'string',
},
{
title: "Notifications push",
key: "notifications",
title: 'Notifications push',
key: 'notifications',
value: !!currentUserData?.notifications,
type: "boolean",
type: 'boolean',
},
{
title: "Notifications par email",
key: "emailNotifications",
title: 'Notifications par email',
key: 'emailNotifications',
value: !!currentUserData?.emailNotifications,
type: "boolean",
type: 'boolean',
},
{
title: "Langue",
key: "language",
value: currentUserData?.language || "",
type: "string",
title: 'Langue',
key: 'language',
value: currentUserData?.language || '',
type: 'string',
},
];
]
const destructiveSettingsList = [
{
title: "Supprimer mon compte",
title: 'Supprimer mon compte',
action: () => onDeleteUserAccount(),
textStyle: {
color: Palette.red,
},
},
].filter(({ condition = true }) => condition);
].filter(({ condition = true }) => condition)
const onChangePicture = async () => {
try {
setIsLoading(true);
setIsLoading(true)
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 4],
quality: 1,
});
})
if (result?.assets?.[0]?.uri) {
const { resultURI = null } = await uploadFileToFirebase({
uri: result?.assets?.[0]?.uri,
path: `users/${currentUID}/profilePicture.png`,
});
})
if (resultURI) {
await usersRef.doc(currentUID).update({
profilePictureURL: resultURI,
});
})
await firebase.auth().currentUser.updateProfile({
photoURL: resultURI,
});
})
setTooltip({
type: "success",
text: "Photo de profil mise à jour",
});
type: 'success',
text: 'Photo de profil mise à jour',
})
} else {
throw new Error("Erreur changement photo de profil");
throw new Error('Erreur changement photo de profil')
}
}
} catch (error) {
console.log(error);
console.log(error)
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
const onUpdateValue = async ({
key,
value = null,
currentPassword = null,
}) => {
const onUpdateValue = async ({ key, value = null, currentPassword = null }) => {
try {
setIsLoading(true);
const user = firebase.auth().currentUser;
setIsLoading(true)
const user = firebase.auth().currentUser
const reauthenticateUser = async () => {
try {
const credential = firebase.auth.EmailAuthProvider.credential(
user?.email,
currentPassword
);
)
await user.reauthenticateWithCredential(credential);
await user.reauthenticateWithCredential(credential)
} catch (error) {
throw new Error("Erreur authentification");
throw new Error('Erreur authentification')
}
};
}
// Validate input except for booleans where false is allowed
const isInvalidString =
typeof value === "string" && value.trim().length === 0;
const isNullish = value === null || value === undefined;
const isInvalidString = typeof value === 'string' && value.trim().length === 0
const isNullish = value === null || value === undefined
if (isNullish || isInvalidString) {
throw new Error("Vérifiez les informations saisies");
throw new Error('Vérifiez les informations saisies')
}
if (key === "name") {
if (key === 'name') {
await user.updateProfile({
displayName: value,
});
})
}
if (["email", "password"].includes(key)) {
await reauthenticateUser();
if (['email', 'password'].includes(key)) {
await reauthenticateUser()
if (key === "email") {
if (key === 'email') {
try {
await user.updateEmail(value);
await user.updateEmail(value)
} catch (e) {
throw new Error("Erreur changement email");
throw new Error('Erreur changement email')
}
}
if (key === "password") {
if (key === 'password') {
try {
await user.updatePassword(value);
await user.updatePassword(value)
} catch (e) {
throw new Error("Erreur changement mot de passe");
throw new Error('Erreur changement mot de passe')
}
}
}
if (key !== "password") {
if (key !== 'password') {
await usersRef.doc(currentUID).update({
[key]: value,
});
})
}
setTooltip({
type: "success",
text: "Modifications enregistrées",
});
type: 'success',
text: 'Modifications enregistrées',
})
} catch (error) {
console.log(error);
console.log(error)
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
const onDeleteUserAccount = async () => {
alert(
"Êtes-vous sûr?",
"La suppression de votre compte entraînera la suppression de toutes les données liées.",
'Êtes-vous sûr?',
'La suppression de votre compte entraînera la suppression de toutes les données liées.',
[
{
text: "Annuler",
text: 'Annuler',
onPress: () => {},
style: "cancel",
style: 'cancel',
},
{
text: "Confirmer",
text: 'Confirmer',
onPress: async () => {
try {
setIsLoading(true);
setIsLoading(true)
await firebase
.functions()
.httpsCallable("users-deleteUserAccount")();
await firebase.functions().httpsCallable('users-deleteUserAccount')()
await onSignOut();
await onSignOut()
setTooltip({
type: "success",
text: "Compte supprimé avec succès!",
});
type: 'success',
text: 'Compte supprimé avec succès!',
})
navigation.reset({
index: 0,
@@ -233,25 +226,25 @@ export default ({ navigation }) => {
name: Routes.Login,
},
],
});
})
} catch (error) {
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
},
style: "confirm",
style: 'confirm',
},
],
{ cancelable: false }
);
};
)
}
return (
<Page headerType="NAVIGATE" title={"Paramètres du compte"} scrollEnabled>
<Page headerType="NAVIGATE" title={'Paramètres du compte'} scrollEnabled>
<Pressable
onPress={onChangePicture}
style={[Style.containerCenter, { marginBottom: gutters }]}
@@ -268,8 +261,8 @@ export default ({ navigation }) => {
/>
<Text
style={Fonts({
type: "section",
style: { color: Palette.primary, textDecorationLine: "underline" },
type: 'section',
style: { color: Palette.primary, textDecorationLine: 'underline' },
})}
>
Modifier
@@ -277,7 +270,7 @@ export default ({ navigation }) => {
</Pressable>
{settingsList.map((setting) => {
const { title, key, value, type } = setting;
const { title, key, value, type } = setting
return (
<InputRow
@@ -288,11 +281,11 @@ export default ({ navigation }) => {
title={title}
onUpdateValue={onUpdateValue}
/>
);
)
})}
<ItemRowList
title={"Gérer mon abonnement"}
title={'Gérer mon abonnement'}
action={() => navigation.navigate(Routes.ManageSubscription)}
containerStyle={{}}
/>
@@ -301,5 +294,5 @@ export default ({ navigation }) => {
<ItemRowList key={index} {...setting} containerStyle={{}} />
))}
</Page>
);
};
)
}
+21 -21
View File
@@ -1,29 +1,29 @@
import React, { useState, useGlobal } from "reactn";
import { useKeyboard } from "@react-native-community/hooks";
import React, { useState, useGlobal } from 'reactn'
import { useKeyboard } from '@react-native-community/hooks'
import { responsiveHeight } from "../actions/responsiveSizes.js";
import { responsiveHeight } from '../actions/responsiveSizes.js'
import Page from "../layouts/Page";
import Page from '../layouts/Page'
import ChatInterface, { onSendMessage } from "../components/ChatInterface";
import ChatInput from "../components/ChatInput.js";
import useLayoutType from "../hooks/useLayoutType.js";
import ChatInterface, { onSendMessage } from '../components/ChatInterface'
import ChatInput from '../components/ChatInput.js'
import useLayoutType from '../hooks/useLayoutType.js'
import { gutters } from "../styles/Style.js";
import { gutters } from '../styles/Style.js'
export default ({ navigation }) => {
const [currentUID] = useGlobal("currentUID");
const [currentProjectID] = useGlobal("currentProjectID");
const [currentUID] = useGlobal('currentUID')
const [currentProjectID] = useGlobal('currentProjectID')
const [message, setMessage] = useState("");
const [message, setMessage] = useState('')
const chatID = `TEST_CHAT_ID`;
const chatID = `TEST_CHAT_ID`
const { isDesktop } = useLayoutType();
const { keyboardShown = false, keyboardHeight = 0 } = useKeyboard();
const { isDesktop } = useLayoutType()
const { keyboardShown = false, keyboardHeight = 0 } = useKeyboard()
return (
<Page headerType={isDesktop ? "NONE" : "BASE"}>
<Page headerType={isDesktop ? 'NONE' : 'BASE'}>
<ChatInterface
hasChatbot
chatID={chatID}
@@ -43,17 +43,17 @@ export default ({ navigation }) => {
setMessage,
chatID,
customPayload: { ...customPayload },
});
})
}}
placeholder={"Laissez un commentaire..."}
placeholder={'Laissez un commentaire...'}
containerStyle={{
position: "absolute",
position: 'absolute',
bottom: gutters * 6 + (keyboardShown ? keyboardHeight : 0),
right: 0,
left: 0,
width: "auto",
width: 'auto',
}}
/>
</Page>
);
};
)
}
+167 -195
View File
@@ -1,117 +1,109 @@
import { useRoute } from "@react-navigation/native";
import React, { useState } from "react";
import { Modal, Pressable, ScrollView, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background } from "../assets";
import BorderGradientButton from "../components/BorderGradientButton";
import GradientButton from "../components/GradientButton";
import { Input } from "../components/Input";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import {
checkIfPasswordIsStrongEnough,
handleFirebaseError,
} from "../actions/signupActions";
import firebase, { usersRef } from "../config/firebase";
import Page from "../layouts/Page";
import { isWeb } from "../hooks/useLayoutType";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { useRoute } from '@react-navigation/native'
import React, { useState } from 'react'
import { Modal, Pressable, ScrollView, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background } from '../assets'
import BorderGradientButton from '../components/BorderGradientButton'
import GradientButton from '../components/GradientButton'
import { Input } from '../components/Input'
import ItemContainer from '../components/ItemContainer/ItemContainer'
import { checkIfPasswordIsStrongEnough, handleFirebaseError } from '../actions/signupActions'
import firebase, { usersRef } from '../config/firebase'
import Page from '../layouts/Page'
import { isWeb } from '../hooks/useLayoutType'
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
const CreatePassword = () => {
const route = useRoute();
const email = route?.params?.email || "";
const firstName = route?.params?.firstName || "";
const lastName = route?.params?.lastName || "";
const city = route?.params?.city || "";
const country = route?.params?.country || null;
const preferredLanguage = route?.params?.preferredLanguage || null;
const birthDate = route?.params?.birthDate || null;
const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState("");
const [loading, setLoading] = useState(false);
const [isCguAccepted, setIsCguAccepted] = useState(false);
const [isCguModalVisible, setIsCguModalVisible] = useState(false);
const [hasScrolledCguToEnd, setHasScrolledCguToEnd] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const route = useRoute()
const email = route?.params?.email || ''
const firstName = route?.params?.firstName || ''
const lastName = route?.params?.lastName || ''
const city = route?.params?.city || ''
const country = route?.params?.country || null
const preferredLanguage = route?.params?.preferredLanguage || null
const birthDate = route?.params?.birthDate || null
const [password, setPassword] = useState('')
const [passwordError, setPasswordError] = useState('')
const [loading, setLoading] = useState(false)
const [isCguAccepted, setIsCguAccepted] = useState(false)
const [isCguModalVisible, setIsCguModalVisible] = useState(false)
const [hasScrolledCguToEnd, setHasScrolledCguToEnd] = useState(false)
const [, setTooltip] = useGlobal('_tooltip')
const handlePasswordChange = (text) => {
setPassword(text);
setPassword(text)
if (!text) {
setPasswordError("");
return;
setPasswordError('')
return
}
if (!checkIfPasswordIsStrongEnough({ password: text })) {
setPasswordError(
"Le mot de passe doit contenir au moins une majuscule, une minuscule, un chiffre et avoir une longueur minimale de 6 caractères",
);
'Le mot de passe doit contenir au moins une majuscule, une minuscule, un chiffre et avoir une longueur minimale de 6 caractères'
)
} else {
setPasswordError("");
setPasswordError('')
}
};
}
const isPasswordValid = checkIfPasswordIsStrongEnough({ password });
const isPasswordValid = checkIfPasswordIsStrongEnough({ password })
const handleToggleCguAccepted = () => {
setIsCguAccepted((prev) => !prev);
};
setIsCguAccepted((prev) => !prev)
}
const handleOpenCguModal = () => {
setHasScrolledCguToEnd(false);
setIsCguModalVisible(true);
};
setHasScrolledCguToEnd(false)
setIsCguModalVisible(true)
}
const handleCloseCguModal = () => {
setIsCguModalVisible(false);
setHasScrolledCguToEnd(false);
};
setIsCguModalVisible(false)
setHasScrolledCguToEnd(false)
}
const handleCguScroll = ({ nativeEvent }) => {
const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
const paddingToBottom = 20;
const contentFits = contentSize.height <= layoutMeasurement.height + 1;
const { layoutMeasurement, contentOffset, contentSize } = nativeEvent
const paddingToBottom = 20
const contentFits = contentSize.height <= layoutMeasurement.height + 1
const isAtEnd =
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom;
layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom
if (isAtEnd) {
setHasScrolledCguToEnd(true);
setHasScrolledCguToEnd(true)
} else if (contentFits) {
setHasScrolledCguToEnd(true);
setHasScrolledCguToEnd(true)
}
};
}
const onCreateAccount = async () => {
try {
if (!isPasswordValid) {
setTooltip({
text: "Le mot de passe doit contenir au moins une majuscule, une minuscule, un chiffre et avoir une longueur minimale de 6 caractères",
type: "error",
});
return;
text: 'Le mot de passe doit contenir au moins une majuscule, une minuscule, un chiffre et avoir une longueur minimale de 6 caractères',
type: 'error',
})
return
}
if (!isCguAccepted) {
setTooltip({
text: "Tu dois accepter les CGU pour continuer",
type: "error",
});
return;
text: 'Tu dois accepter les CGU pour continuer',
type: 'error',
})
return
}
setLoading(true);
const cred = await firebase
.auth()
.createUserWithEmailAndPassword(email.trim(), password);
const uid = cred?.user?.uid || firebase.auth().currentUser?.uid;
if (!uid) throw new Error("Création de compte échouée");
const trimmedFirstName =
typeof firstName === "string" ? firstName.trim() : "";
const trimmedLastName =
typeof lastName === "string" ? lastName.trim() : "";
const trimmedCity = typeof city === "string" ? city.trim() : "";
setLoading(true)
const cred = await firebase.auth().createUserWithEmailAndPassword(email.trim(), password)
const uid = cred?.user?.uid || firebase.auth().currentUser?.uid
if (!uid) throw new Error('Création de compte échouée')
const trimmedFirstName = typeof firstName === 'string' ? firstName.trim() : ''
const trimmedLastName = typeof lastName === 'string' ? lastName.trim() : ''
const trimmedCity = typeof city === 'string' ? city.trim() : ''
await usersRef.doc(uid).set(
{
email: email.trim(),
@@ -126,21 +118,21 @@ const CreatePassword = () => {
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
setTooltip({ text: "Compte créé, continuons", type: "success" });
navigate(Routes.BottomTab);
{ merge: true }
)
setTooltip({ text: 'Compte créé, continuons', type: 'success' })
navigate(Routes.BottomTab)
} catch (e) {
console.log("Register error", e?.message);
console.log('Register error', e?.message)
const message =
e?.code && e.code.startsWith("auth/")
e?.code && e.code.startsWith('auth/')
? handleFirebaseError(e.code)
: e?.message || "Création impossible";
setTooltip({ text: message, type: "error" });
: e?.message || 'Création impossible'
setTooltip({ text: message, type: 'error' })
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
@@ -153,14 +145,14 @@ const CreatePassword = () => {
style={{
flex: 1,
paddingVertical: 20,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
}}
>
<ItemContainer
height={isWeb ? 360 : responsiveHeight(65)}
disableKeyboardHeight
style={{ width: "100%" }}
style={{ width: '100%' }}
>
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
<View style={{ gap: 16 }}>
@@ -172,7 +164,7 @@ const CreatePassword = () => {
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
Choisis un mot de passe{"\n"}sécurisé
Choisis un mot de passe{'\n'}sécurisé
</Text>
<Text
style={{
@@ -207,17 +199,17 @@ const CreatePassword = () => {
<View style={{ gap: 12 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 8,
flexWrap: "wrap",
flexWrap: 'wrap',
}}
>
<Pressable
onPress={handleToggleCguAccepted}
style={{
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}}
>
@@ -225,8 +217,8 @@ const CreatePassword = () => {
style={{
width: 17,
height: 17,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderRadius: 100,
borderColor: Palette.white,
@@ -259,7 +251,7 @@ const CreatePassword = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textDecorationLine: "underline",
textDecorationLine: 'underline',
}}
>
CGU
@@ -268,15 +260,13 @@ const CreatePassword = () => {
</View>
</View>
<GradientButton
title={loading ? "Création..." : "Créer mon compte"}
title={loading ? 'Création...' : 'Créer mon compte'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onCreateAccount}
disabled={
loading || !email || !isPasswordValid || !isCguAccepted
}
disabled={loading || !email || !isPasswordValid || !isCguAccepted}
/>
</View>
</ItemContainer>
@@ -291,10 +281,10 @@ const CreatePassword = () => {
style={{
flex: 1,
backgroundColor: Palette.transparentBlack,
justifyContent: "center",
justifyContent: 'center',
paddingHorizontal: 20,
paddingVertical: 40,
alignItems: "center",
alignItems: 'center',
}}
>
<View
@@ -303,15 +293,15 @@ const CreatePassword = () => {
borderRadius: 16,
padding: 20,
paddingTop: 28,
maxHeight: "75%",
width: "90%",
maxHeight: '75%',
width: '90%',
flexShrink: 1,
}}
>
<Pressable
onPress={handleCloseCguModal}
style={{
position: "absolute",
position: 'absolute',
top: 12,
right: 12,
padding: 6,
@@ -334,7 +324,7 @@ const CreatePassword = () => {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Conditions Générales d&apos;Utilisation
@@ -355,15 +345,13 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Cupidatat incididunt aute aute velit deserunt labore excepteur
eu velit cillum est. Laboris in sunt ea Lorem culpa velit nisi
ad elit esse voluptate id adipisicing nulla magna. Duis qui
fugiat consequat elit velit aute ad. Reprehenderit proident
duis exercitation velit proident aute minim. Anim
reprehenderit mollit consectetur id sint adipisicing pariatur
tempor sit pariatur proident nostrud. Sit culpa tempor enim ut
consectetur sunt aute est reprehenderit incididunt incididunt
in ullamco consectetur. Eiusmod non incididunt proident
Cupidatat incididunt aute aute velit deserunt labore excepteur eu velit cillum
est. Laboris in sunt ea Lorem culpa velit nisi ad elit esse voluptate id
adipisicing nulla magna. Duis qui fugiat consequat elit velit aute ad.
Reprehenderit proident duis exercitation velit proident aute minim. Anim
reprehenderit mollit consectetur id sint adipisicing pariatur tempor sit pariatur
proident nostrud. Sit culpa tempor enim ut consectetur sunt aute est reprehenderit
incididunt incididunt in ullamco consectetur. Eiusmod non incididunt proident
eiusmod. Esse Lorem ut amet in est id aute consectetur.
</Text>
<Text
@@ -374,13 +362,12 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Aliqua est sint consectetur occaecat exercitation. Elit nisi
velit ad qui eiusmod reprehenderit proident labore nostrud
labore consequat enim fugiat non. Eiusmod occaecat pariatur
deserunt velit elit sint irure fugiat excepteur labore ad
velit ex deserunt laborum. Aliqua excepteur reprehenderit
nostrud aliqua pariatur aliqua excepteur Lorem consectetur. Ut
est culpa ullamco ipsum Lorem ullamco ea labore anim.
Aliqua est sint consectetur occaecat exercitation. Elit nisi velit ad qui eiusmod
reprehenderit proident labore nostrud labore consequat enim fugiat non. Eiusmod
occaecat pariatur deserunt velit elit sint irure fugiat excepteur labore ad velit
ex deserunt laborum. Aliqua excepteur reprehenderit nostrud aliqua pariatur aliqua
excepteur Lorem consectetur. Ut est culpa ullamco ipsum Lorem ullamco ea labore
anim.
</Text>
<Text
style={{
@@ -390,15 +377,13 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Et ut ex duis tempor amet aliquip cupidatat sit sit. Aliqua
consectetur amet duis fugiat nulla duis culpa exercitation
reprehenderit. Ea voluptate proident ad elit pariatur do.
Culpa incididunt deserunt dolor ut officia aliquip ut
occaecat eiusmod sint veniam. Deserunt consequat adipisicing
aliquip velit labore fugiat aute culpa id sint. Adipisicing eu
commodo ex do aliqua in labore laboris sit sunt adipisicing
excepteur. Do sint velit veniam pariatur consequat proident
cupidatat in incididunt ullamco.
Et ut ex duis tempor amet aliquip cupidatat sit sit. Aliqua consectetur amet duis
fugiat nulla duis culpa exercitation reprehenderit. Ea voluptate proident ad elit
pariatur do. Culpa incididunt deserunt dolor ut officia aliquip ut occaecat
eiusmod sint veniam. Deserunt consequat adipisicing aliquip velit labore fugiat
aute culpa id sint. Adipisicing eu commodo ex do aliqua in labore laboris sit sunt
adipisicing excepteur. Do sint velit veniam pariatur consequat proident cupidatat
in incididunt ullamco.
</Text>
<Text
style={{
@@ -408,11 +393,10 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Culpa eu cillum fugiat elit eiusmod enim. Cupidatat culpa
aliquip culpa et tempor est velit. Aute id deserunt non ut
minim deserunt adipisicing sit veniam eu id incididunt
adipisicing dolore. Eiusmod nisi excepteur est voluptate
consequat reprehenderit non exercitation commodo aute.
Culpa eu cillum fugiat elit eiusmod enim. Cupidatat culpa aliquip culpa et tempor
est velit. Aute id deserunt non ut minim deserunt adipisicing sit veniam eu id
incididunt adipisicing dolore. Eiusmod nisi excepteur est voluptate consequat
reprehenderit non exercitation commodo aute.
</Text>
<Text
style={{
@@ -422,12 +406,11 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Eu culpa enim dolore anim ipsum eu veniam mollit dolore
officia cupidatat laborum officia do. Ad proident in anim nisi
exercitation consequat exercitation occaecat consequat ut
laborum esse consectetur ullamco. Enim elit sit in id velit
quis nostrud nostrud eu labore aute reprehenderit voluptate
deserunt. Minim voluptate et duis voluptate enim duis.
Eu culpa enim dolore anim ipsum eu veniam mollit dolore officia cupidatat laborum
officia do. Ad proident in anim nisi exercitation consequat exercitation occaecat
consequat ut laborum esse consectetur ullamco. Enim elit sit in id velit quis
nostrud nostrud eu labore aute reprehenderit voluptate deserunt. Minim voluptate
et duis voluptate enim duis.
</Text>
<Text
style={{
@@ -437,9 +420,9 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Minim laboris deserunt minim ex. Duis aliquip cillum proident.
Magna velit nisi fugiat. Non id proident pariatur elit in
exercitation et amet id qui ad laborum nulla ullamco ea.
Minim laboris deserunt minim ex. Duis aliquip cillum proident. Magna velit nisi
fugiat. Non id proident pariatur elit in exercitation et amet id qui ad laborum
nulla ullamco ea.
</Text>
<Text
style={{
@@ -449,13 +432,11 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Cillum nisi aute dolore culpa est veniam cupidatat sunt sint
ipsum. Amet ullamco minim non voluptate cillum dolore sunt
irure nulla pariatur excepteur voluptate id. Commodo ullamco
aliqua non ea mollit ullamco do minim dolor magna. Ipsum eu
minim quis laborum do dolore labore eiusmod et. Ullamco
officia velit anim. Exercitation voluptate reprehenderit ex et
do eu fugiat tempor do cillum ad.
Cillum nisi aute dolore culpa est veniam cupidatat sunt sint ipsum. Amet ullamco
minim non voluptate cillum dolore sunt irure nulla pariatur excepteur voluptate
id. Commodo ullamco aliqua non ea mollit ullamco do minim dolor magna. Ipsum eu
minim quis laborum do dolore labore eiusmod et. Ullamco officia velit anim.
Exercitation voluptate reprehenderit ex et do eu fugiat tempor do cillum ad.
</Text>
<Text
style={{
@@ -465,18 +446,15 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Cupidatat consectetur excepteur commodo laborum incididunt
laboris minim dolore ut ipsum dolor ullamco culpa aliquip.
Cillum proident quis consectetur voluptate labore nisi Lorem
pariatur in esse. Amet ipsum mollit officia fugiat Lorem ipsum
elit officia. Esse reprehenderit magna quis irure sit
consectetur dolore sunt mollit aliquip eiusmod voluptate amet.
Ex irure culpa ea cupidatat nulla ea labore aute occaecat
consequat consectetur cillum amet. Velit ad do occaecat non
elit quis. Amet id reprehenderit ullamco amet tempor deserunt
exercitation elit consectetur minim aliqua. Cillum do aliquip
do ea ipsum veniam deserunt in ipsum pariatur nisi proident et
ut deserunt.
Cupidatat consectetur excepteur commodo laborum incididunt laboris minim dolore ut
ipsum dolor ullamco culpa aliquip. Cillum proident quis consectetur voluptate
labore nisi Lorem pariatur in esse. Amet ipsum mollit officia fugiat Lorem ipsum
elit officia. Esse reprehenderit magna quis irure sit consectetur dolore sunt
mollit aliquip eiusmod voluptate amet. Ex irure culpa ea cupidatat nulla ea labore
aute occaecat consequat consectetur cillum amet. Velit ad do occaecat non elit
quis. Amet id reprehenderit ullamco amet tempor deserunt exercitation elit
consectetur minim aliqua. Cillum do aliquip do ea ipsum veniam deserunt in ipsum
pariatur nisi proident et ut deserunt.
</Text>
<Text
style={{
@@ -486,15 +464,13 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Mollit et adipisicing velit tempor deserunt excepteur fugiat
eiusmod adipisicing. Tempor do ex duis dolor deserunt cillum
officia est nisi mollit fugiat. Amet duis ea laboris officia
aliquip id sint voluptate consectetur velit elit
reprehenderit nostrud exercitation. Ea dolore adipisicing nulla
incididunt laboris sint commodo non mollit eiusmod. Tempor
nulla eu laborum tempor veniam laboris consequat non consequat
exercitation pariatur velit. Voluptate magna mollit esse
incididunt id. Pariatur eu irure esse ullamco fugiat culpa.
Mollit et adipisicing velit tempor deserunt excepteur fugiat eiusmod adipisicing.
Tempor do ex duis dolor deserunt cillum officia est nisi mollit fugiat. Amet duis
ea laboris officia aliquip id sint voluptate consectetur velit elit reprehenderit
nostrud exercitation. Ea dolore adipisicing nulla incididunt laboris sint commodo
non mollit eiusmod. Tempor nulla eu laborum tempor veniam laboris consequat non
consequat exercitation pariatur velit. Voluptate magna mollit esse incididunt id.
Pariatur eu irure esse ullamco fugiat culpa.
</Text>
<Text
style={{
@@ -504,11 +480,10 @@ const CreatePassword = () => {
lineHeight: 20,
}}
>
Sint dolore aliqua pariatur do mollit occaecat deserunt qui
proident non exercitation mollit cillum culpa. Est cupidatat
consequat ea commodo laborum labore sunt excepteur labore aute
est amet anim. Tempor deserunt labore mollit enim officia
aliqua occaecat. Ullamco mollit qui mollit do ex irure enim.
Sint dolore aliqua pariatur do mollit occaecat deserunt qui proident non
exercitation mollit cillum culpa. Est cupidatat consequat ea commodo laborum
labore sunt excepteur labore aute est amet anim. Tempor deserunt labore mollit
enim officia aliqua occaecat. Ullamco mollit qui mollit do ex irure enim.
</Text>
</ScrollView>
{hasScrolledCguToEnd ? (
@@ -516,14 +491,11 @@ const CreatePassword = () => {
<GradientButton
title="Accepter sans réserve"
onPress={() => {
setIsCguAccepted(true);
handleCloseCguModal();
setIsCguAccepted(true)
handleCloseCguModal()
}}
/>
<BorderGradientButton
title="Refuser"
onPress={handleCloseCguModal}
/>
<BorderGradientButton title="Refuser" onPress={handleCloseCguModal} />
</View>
) : (
<Text
@@ -531,7 +503,7 @@ const CreatePassword = () => {
fontSize: 12,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Fais défiler jusqu&apos;en bas pour afficher les actions
@@ -542,7 +514,7 @@ const CreatePassword = () => {
</View>
</Modal>
</Page>
);
};
)
}
export default CreatePassword;
export default CreatePassword
+42 -45
View File
@@ -1,42 +1,39 @@
import React, { useState } from "react";
import { Text, View } from "react-native";
import { useGlobal } from "reactn";
import { background } from "../assets";
import GradientButton from "../components/GradientButton";
import { Input } from "../components/Input";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import firebase, { usersRef } from "../config/firebase";
import Page from "../layouts/Page";
import { isWeb } from "../hooks/useLayoutType";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import React, { useState } from 'react'
import { Text, View } from 'react-native'
import { useGlobal } from 'reactn'
import { background } from '../assets'
import GradientButton from '../components/GradientButton'
import { Input } from '../components/Input'
import ItemContainer from '../components/ItemContainer/ItemContainer'
import firebase, { usersRef } from '../config/firebase'
import Page from '../layouts/Page'
import { isWeb } from '../hooks/useLayoutType'
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
const CreatePseudo = () => {
const [userName, setUserName] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const [userName, setUserName] = useState('')
const [loading, setLoading] = useState(false)
const [, setTooltip] = useGlobal('_tooltip')
const onCreate = async () => {
const raw = userName || "";
const val = raw.trim();
if (!val) return;
const raw = userName || ''
const val = raw.trim()
if (!val) return
try {
setLoading(true);
const uid = firebase.auth().currentUser?.uid;
setLoading(true)
const uid = firebase.auth().currentUser?.uid
if (!uid) {
return navigate(Routes.Login);
return navigate(Routes.Login)
}
// Check uniqueness (case-insensitive)
const lower = val.toLowerCase();
const snap = await usersRef
.where("userNameLower", "==", lower)
.limit(1)
.get();
const lower = val.toLowerCase()
const snap = await usersRef.where('userNameLower', '==', lower).limit(1).get()
if (!snap.empty && snap.docs[0].id !== uid) {
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
return;
setTooltip({ text: 'Ce pseudo est déjà pris', type: 'error' })
return
}
await usersRef.doc(uid).set(
@@ -48,19 +45,19 @@ const CreatePseudo = () => {
email: firebase.auth().currentUser?.email || null,
},
{ merge: true }
);
setTooltip({ text: `Bienvenue ${val}`, type: "success" });
navigate(Routes.BottomTab);
)
setTooltip({ text: `Bienvenue ${val}`, type: 'success' })
navigate(Routes.BottomTab)
} catch (e) {
console.log("CreatePseudo error", e?.message);
console.log('CreatePseudo error', e?.message)
setTooltip({
text: e?.message || "Enregistrement impossible",
type: "error",
});
text: e?.message || 'Enregistrement impossible',
type: 'error',
})
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
@@ -103,10 +100,10 @@ const CreatePseudo = () => {
/>
</View>
<GradientButton
title={loading ? "Chargement..." : "Valider"}
title={loading ? 'Chargement...' : 'Valider'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onCreate}
disabled={loading || !userName?.trim()}
@@ -115,7 +112,7 @@ const CreatePseudo = () => {
</ItemContainer>
</View>
</Page>
);
};
)
}
export default CreatePseudo;
export default CreatePseudo
+49 -65
View File
@@ -1,52 +1,50 @@
import { View, Text, Image, Pressable, Platform } from "react-native";
import React, { useMemo, useState } from "react";
import Page from "../layouts/Page";
import { ai, background, icons } from "../assets";
import { Palette, Style } from "../styles";
import { responsiveHeight } from "react-native-responsive-dimensions";
import BorderGradient from "../components/BorderGradient/BorderGradient";
import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../styles/Fonts";
import { size } from "../styles/Style";
import GradientButton from "../components/GradientButton";
import { goBack, navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
import { useUser } from "../providers/UserDataProvider";
import { View, Text, Image, Pressable, Platform } from 'react-native'
import React, { useMemo, useState } from 'react'
import Page from '../layouts/Page'
import { ai, background, icons } from '../assets'
import { Palette, Style } from '../styles'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import BorderGradient from '../components/BorderGradient/BorderGradient'
import { BlurView } from 'expo-blur'
import { FONT_FAMILY } from '../styles/Fonts'
import { size } from '../styles/Style'
import GradientButton from '../components/GradientButton'
import { goBack, navigate } from '../navigation/NavigationService'
import { Routes } from '../navigation'
import { useUser } from '../providers/UserDataProvider'
const CreateSongs = () => {
const [selectedIndex, setSelectedIndex] = useState(null);
const { userProjects = [] } = useUser();
const [selectedIndex, setSelectedIndex] = useState(null)
const { userProjects = [] } = useUser()
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects.slice(0, 3) : []),
[userProjects]
);
)
const fmtDate = (ts) => {
try {
const d = ts?.toDate ? ts.toDate() : ts ? new Date(ts) : null;
if (!d || Number.isNaN(d.getTime())) return "";
const dd = `${d.getDate().toString().padStart(2, "0")}/${(
d.getMonth() + 1
)
const d = ts?.toDate ? ts.toDate() : ts ? new Date(ts) : null
if (!d || Number.isNaN(d.getTime())) return ''
const dd = `${d.getDate().toString().padStart(2, '0')}/${(d.getMonth() + 1)
.toString()
.padStart(2, "0")}`;
return dd;
.padStart(2, '0')}`
return dd
} catch (_) {
return "";
return ''
}
};
}
return (
<Page backgroundImg={background.homeBG} headerType="NONE">
<View style={{ flex: 1, ...Style.containerCenter }}>
<View
style={{
width: "100%",
width: '100%',
height: responsiveHeight(60),
}}
>
<BorderGradient
gradientProps={{
colors: ["#FFFFFF00", Palette.white],
colors: ['#FFFFFF00', Palette.white],
start: { x: 0, y: 0 },
end: { x: 1, y: 0.4 },
}}
@@ -61,20 +59,20 @@ const CreateSongs = () => {
style={{
width: 171,
height: 250,
position: "absolute",
position: 'absolute',
zIndex: 3,
top: -100,
alignSelf: "center",
alignSelf: 'center',
}}
resizeMode="contain"
/>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
intensity={Platform.OS !== 'ios' ? 10 : 40}
style={{
flex: 1,
borderRadius: 20,
overflow: "hidden",
justifyContent: "flex-end",
overflow: 'hidden',
justifyContent: 'flex-end',
backgroundColor: Palette.glass,
paddingHorizontal: 10,
zIndex: 1,
@@ -87,7 +85,7 @@ const CreateSongs = () => {
>
<Pressable
style={{
position: "absolute",
position: 'absolute',
...Style.containerCenter,
...size({ size: 24 }),
top: 10,
@@ -102,7 +100,7 @@ const CreateSongs = () => {
}}
/>
</Pressable>
<View style={{ alignItems: "center", zIndex: 2 }}>
<View style={{ alignItems: 'center', zIndex: 2 }}>
<Text
style={{
fontSize: 22,
@@ -127,24 +125,24 @@ const CreateSongs = () => {
<Pressable
key={index}
onPress={() => {
console.log("Pressed");
console.log('Pressed')
if (selectedIndex === index) {
setSelectedIndex(null);
setSelectedIndex(null)
} else {
setSelectedIndex(index);
setSelectedIndex(index)
}
}}
style={{ zIndex: 1 }}
>
<BlurView
tint={selectedIndex === index ? "default" : "dark"}
tint={selectedIndex === index ? 'default' : 'dark'}
style={{
...Style.containerSpaceBetween,
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 14,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -154,22 +152,16 @@ const CreateSongs = () => {
<Text
style={{
fontSize: 16,
color:
selectedIndex === index
? Palette.black
: Palette.white,
color: selectedIndex === index ? Palette.black : Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item?.title || "Sans titre"}
{item?.title || 'Sans titre'}
</Text>
<Text
style={{
fontSize: 12,
color:
selectedIndex === index
? Palette.black
: Palette.white,
color: selectedIndex === index ? Palette.black : Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
@@ -181,13 +173,9 @@ const CreateSongs = () => {
source={icons.threeDots}
style={{
...size({ size: 24 }),
transform: [{ rotateZ: "90deg" }],
transform: [{ rotateZ: '90deg' }],
}}
tintColor={
selectedIndex === index
? Palette.black
: Palette.white
}
tintColor={selectedIndex === index ? Palette.black : Palette.white}
resizeMode="contain"
/>
</Pressable>
@@ -196,14 +184,10 @@ const CreateSongs = () => {
))}
</View>
<GradientButton
title={
selectedIndex !== null
? "Poursuivre la création"
: "Créer un texte"
}
title={selectedIndex !== null ? 'Poursuivre la création' : 'Créer un texte'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
marginTop: 10,
}}
onPress={() => navigate(Routes.Writing)}
@@ -213,7 +197,7 @@ const CreateSongs = () => {
</View>
</View>
</Page>
);
};
)
}
export default CreateSongs;
export default CreateSongs
+48 -55
View File
@@ -1,99 +1,92 @@
import React, { useState, useGlobal } from "reactn";
import { Text, KeyboardAvoidingView } from "react-native";
import { responsiveHeight } from "../actions/responsiveSizes.js";
import React, { useState, useGlobal } from 'reactn'
import { Text, KeyboardAvoidingView } from 'react-native'
import { responsiveHeight } from '../actions/responsiveSizes.js'
import firebase from "../config/firebase";
import firebase from '../config/firebase'
import Button from "../components/Button";
import { Input } from "../components/Input";
import Button from '../components/Button'
import { Input } from '../components/Input'
import { Fonts, Style } from "../styles";
import Page from "../layouts/Page";
import { background } from "../assets";
import { isWeb } from "../hooks/useLayoutType";
import {
checkIfEmailIsValid,
handleFirebaseError,
} from "../actions/signupActions.js";
import { Fonts, Style } from '../styles'
import Page from '../layouts/Page'
import { background } from '../assets'
import { isWeb } from '../hooks/useLayoutType'
import { checkIfEmailIsValid, handleFirebaseError } from '../actions/signupActions.js'
export default ({ navigation }) => {
const [, setTooltip] = useGlobal("_tooltip");
const [, setIsLoading] = useGlobal("_isLoading");
const [, setTooltip] = useGlobal('_tooltip')
const [, setIsLoading] = useGlobal('_isLoading')
const [email, setEmail] = useState(__DEV__ ? "hello@minuit.agency" : "");
const [email, setEmail] = useState(__DEV__ ? 'hello@minuit.agency' : '')
const onResetPassword = async () => {
const trimmedEmail = (email || "").trim();
const trimmedEmail = (email || '').trim()
if (!trimmedEmail?.length) {
setTooltip({
text: "Renseigne ton adresse e-mail.",
type: "error",
});
return;
text: 'Renseigne ton adresse e-mail.',
type: 'error',
})
return
}
if (!checkIfEmailIsValid({ email: trimmedEmail })) {
setTooltip({
text: "Adresse e-mail invalide.",
type: "error",
});
return;
text: 'Adresse e-mail invalide.',
type: 'error',
})
return
}
try {
setIsLoading(true);
setIsLoading(true)
await firebase.auth().sendPasswordResetEmail(trimmedEmail);
await firebase.auth().sendPasswordResetEmail(trimmedEmail)
setTooltip({
text: "Email de réinitialisation envoyé!",
type: "success",
});
text: 'Email de réinitialisation envoyé!',
type: 'success',
})
navigation.goBack();
navigation.goBack()
} catch (error) {
console.log("ForgotPassword error", error?.message);
console.log('ForgotPassword error', error?.message)
const message =
error?.code && error.code.startsWith("auth/")
error?.code && error.code.startsWith('auth/')
? handleFirebaseError(error.code)
: error?.message || "Une erreur est survenue";
: error?.message || 'Une erreur est survenue'
setTooltip({
text: message,
type: "error",
});
type: 'error',
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
return (
<Page
width={isWeb ? 600 : null}
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
headerType="NAVIGATE"
title={"Nouveau mot de passe"}
title={'Nouveau mot de passe'}
scrollEnabled
bottomStickyContent={() => (
<Button
isAbsoluteBottom
text="Renvoyer le mot de passe"
onPress={onResetPassword}
/>
<Button isAbsoluteBottom text="Renvoyer le mot de passe" onPress={onResetPassword} />
)}
>
<KeyboardAvoidingView
style={{ ...Style.containerCenter, width: "100%" }}
style={{ ...Style.containerCenter, width: '100%' }}
behavior="padding"
keyboardVerticalOffset={responsiveHeight(10)}
>
<Text
style={{
...Fonts({ type: "default" }),
...Fonts({ type: 'default' }),
marginBottom: responsiveHeight(10),
}}
>
Saisissez votre adresse mail, nous vous enverrons un lien pour
réinitialiser votre mot de passe.
Saisissez votre adresse mail, nous vous enverrons un lien pour réinitialiser votre mot de
passe.
</Text>
<Input
@@ -105,13 +98,13 @@ export default ({ navigation }) => {
isBlur
containerStyle={{ marginBottom: 20 }}
textInputProps={{
keyboardType: "email-address",
autoCapitalize: "none",
autoCompleteType: "email",
textContentType: "emailAddress",
keyboardType: 'email-address',
autoCapitalize: 'none',
autoCompleteType: 'email',
textContentType: 'emailAddress',
}}
/>
</KeyboardAvoidingView>
</Page>
);
};
)
}
+179 -215
View File
@@ -1,10 +1,4 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
FlatList,
Image,
@@ -14,87 +8,78 @@ import {
StyleSheet,
Text,
View,
} from "react-native";
import { BlurView } from "expo-blur";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import SearchBar from "../../components/SearchBar";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import firebase, { projectsRef, usersRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import useSearch from "../../hooks/useSearch";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import MobileCoinBadge from "../../components/MobileCoinBadge";
import ResearchHeader from "../Library/components/ResearchHeader";
import SearchResultsList from "../Library/components/SearchResultsList";
import PlaybacksCard from "./components/PlaybacksCard";
import SongCard from "./components/SongCard";
import BorderGradient from "../../components/BorderGradient/BorderGradient.web";
} from 'react-native'
import { BlurView } from 'expo-blur'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background, icons } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import SearchBar from '../../components/SearchBar'
import ShareBtn from '../../components/ShareBtn/ShareBtn'
import firebase, { projectsRef, usersRef } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import useSearch from '../../hooks/useSearch'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import MobileCoinBadge from '../../components/MobileCoinBadge'
import ResearchHeader from '../Library/components/ResearchHeader'
import SearchResultsList from '../Library/components/SearchResultsList'
import PlaybacksCard from './components/PlaybacksCard'
import SongCard from './components/SongCard'
import BorderGradient from '../../components/BorderGradient/BorderGradient.web'
const allowedPremiumLevels = new Set(["starter", "pro", "premium"]);
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
const normalizePremiumLevel = (value) => {
if (typeof value !== "string") {
return null;
if (typeof value !== 'string') {
return null
}
const normalized = value.trim().toLowerCase();
return allowedPremiumLevels.has(normalized) ? normalized : null;
};
const normalized = value.trim().toLowerCase()
return allowedPremiumLevels.has(normalized) ? normalized : null
}
const chunkArray = (items = [], size = 10) => {
const chunks = [];
const chunks = []
for (let i = 0; i < items.length; i += size) {
chunks.push(items.slice(i, i + size));
chunks.push(items.slice(i, i + size))
}
return chunks;
};
return chunks
}
const HitParade = () => {
const [selectedCategory, setSelectedCategory] = useState("Chansons");
const navigateToMusicDetails = useNavigateToMusicDetails();
const [selectedCategory, setSelectedCategory] = useState('Chansons')
const navigateToMusicDetails = useNavigateToMusicDetails()
const { data: topSongs } = useDataFromRef({
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(20),
ref: projectsRef.where('views', '>', 0).orderBy('views', 'desc').limit(20),
simpleRef: false,
listener: true,
condition: true,
});
})
const { data: topPlaybacks } = useDataFromRef({
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(60),
ref: projectsRef.where('views', '>', 0).orderBy('views', 'desc').limit(60),
format: (docs) => docs.filter((d) => d?.playbackUrl != null).slice(0, 20),
simpleRef: false,
listener: true,
condition: true,
});
})
const songsList = useMemo(
() => (Array.isArray(topSongs) ? topSongs : []),
[topSongs]
);
const songsList = useMemo(() => (Array.isArray(topSongs) ? topSongs : []), [topSongs])
const playbackList = useMemo(
() => (Array.isArray(topPlaybacks) ? topPlaybacks : []),
[topPlaybacks]
);
)
const resolvePlaybackThumbnail = useCallback((project) => {
const candidates = [
project?.thumbnailUrl,
project?.songThumbnailUrl,
project?.coverUrl,
];
const uri = candidates.find(
(value) => typeof value === "string" && value.trim().length > 0
);
return uri || null;
}, []);
const candidates = [project?.thumbnailUrl, project?.songThumbnailUrl, project?.coverUrl]
const uri = candidates.find((value) => typeof value === 'string' && value.trim().length > 0)
return uri || null
}, [])
// === RENDUS PAR PLATEFORME ===
const renderSongsMobile = () => (
@@ -108,7 +93,7 @@ const HitParade = () => {
renderItem={({ item, index }) => (
<SongCard
rank={index + 1}
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
artist={item?.userName}
coverUrl={item?.coverUrl || null}
subscriptionLevel={getCreatorLevel(item)}
@@ -122,7 +107,7 @@ const HitParade = () => {
/>
)}
/>
);
)
const renderPlaybacksMobile = () => (
<FlatList
@@ -135,7 +120,7 @@ const HitParade = () => {
renderItem={({ item, index }) => (
<PlaybacksCard
rank={index + 1}
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
artist={item?.userName}
thumbnailUrl={resolvePlaybackThumbnail(item)}
subscriptionLevel={getCreatorLevel(item)}
@@ -143,7 +128,7 @@ const HitParade = () => {
/>
)}
/>
);
)
const renderSongList = () => (
<View style={{ gap: 10 }}>
@@ -151,7 +136,7 @@ const HitParade = () => {
<SongCard
key={item.id}
rank={idx + 1}
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
artist={item?.userName}
coverUrl={item?.coverUrl || null}
subscriptionLevel={getCreatorLevel(item)}
@@ -165,7 +150,7 @@ const HitParade = () => {
/>
))}
</View>
);
)
const renderPlaybackColumns = () => (
<View style={{ gap: 10 }}>
@@ -173,7 +158,7 @@ const HitParade = () => {
<PlaybacksCard
key={item.id}
rank={idx + 1}
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
artist={item?.userName}
thumbnailUrl={resolvePlaybackThumbnail(item)}
subscriptionLevel={getCreatorLevel(item)}
@@ -181,9 +166,9 @@ const HitParade = () => {
/>
))}
</View>
);
)
const isWeb = Platform.OS === "web";
const isWeb = Platform.OS === 'web'
const {
search,
setSearch,
@@ -193,216 +178,195 @@ const HitParade = () => {
musics = [],
playbacks = [],
loading: searchLoading,
} = useSearch();
} = useSearch()
const [dropdownVisible, setDropdownVisible] = useState(false);
const searchWrapperRef = useRef(null);
const hasSearchQuery = search.trim().length > 0;
const [dropdownVisible, setDropdownVisible] = useState(false)
const searchWrapperRef = useRef(null)
const hasSearchQuery = search.trim().length > 0
const toggleSearchSelected = useCallback(
(value) => {
setSearchSelected((previous) => (previous === value ? null : value));
setSearchSelected((previous) => (previous === value ? null : value))
},
[setSearchSelected]
);
)
const musicResults = useMemo(
() => (Array.isArray(musics) ? musics : []),
[musics]
);
const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics])
const [creatorsById, setCreatorsById] = useState({});
const [creatorsById, setCreatorsById] = useState({})
const requiredUserIds = useMemo(() => {
const collected = new Set();
const collected = new Set()
const collectUserId = (project) => {
const uid =
project && typeof project.userId === "string"
? project.userId.trim()
: null;
const uid = project && typeof project.userId === 'string' ? project.userId.trim() : null
if (uid) {
collected.add(uid);
collected.add(uid)
}
};
songsList.forEach(collectUserId);
playbackList.forEach(collectUserId);
return Array.from(collected);
}, [playbackList, songsList]);
}
songsList.forEach(collectUserId)
playbackList.forEach(collectUserId)
return Array.from(collected)
}, [playbackList, songsList])
const fetchCreatorsByIds = useCallback(
async (userIds) => {
if (!Array.isArray(userIds) || userIds.length === 0) {
return;
return
}
const normalizedIds = userIds
.map((id) => (typeof id === "string" ? id.trim() : null))
.filter(Boolean);
.map((id) => (typeof id === 'string' ? id.trim() : null))
.filter(Boolean)
if (normalizedIds.length === 0) {
return;
return
}
const pendingIds = new Set(normalizedIds);
const nextCreators = {};
const pendingIds = new Set(normalizedIds)
const nextCreators = {}
const idChunks = chunkArray(normalizedIds, 10);
const idChunks = chunkArray(normalizedIds, 10)
await Promise.all(
idChunks.map(async (chunk) => {
try {
const snapshot = await usersRef
.where(firebase.firestore.FieldPath.documentId(), "in", chunk)
.get();
.where(firebase.firestore.FieldPath.documentId(), 'in', chunk)
.get()
snapshot.docs.forEach((doc) => {
const data = doc.data() || {};
const data = doc.data() || {}
nextCreators[doc.id] = {
premiumLevel: normalizePremiumLevel(data.premiumLevel),
};
pendingIds.delete(doc.id);
});
}
pendingIds.delete(doc.id)
})
} catch (error) {
console.log(
"HitParade: unable to fetch creators",
error?.message || error
);
console.log('HitParade: unable to fetch creators', error?.message || error)
}
})
);
)
pendingIds.forEach((userId) => {
nextCreators[userId] = { premiumLevel: null };
});
nextCreators[userId] = { premiumLevel: null }
})
if (Object.keys(nextCreators).length > 0) {
setCreatorsById((previous) => ({ ...previous, ...nextCreators }));
setCreatorsById((previous) => ({ ...previous, ...nextCreators }))
}
},
[setCreatorsById]
);
)
useEffect(() => {
const missingIds = requiredUserIds.filter((id) => !creatorsById[id]);
const missingIds = requiredUserIds.filter((id) => !creatorsById[id])
if (missingIds.length === 0) {
return;
return
}
fetchCreatorsByIds(missingIds);
}, [creatorsById, fetchCreatorsByIds, requiredUserIds]);
fetchCreatorsByIds(missingIds)
}, [creatorsById, fetchCreatorsByIds, requiredUserIds])
const getCreatorLevel = useCallback(
(project) => {
const uid =
project && typeof project.userId === "string"
? project.userId.trim()
: null;
const uid = project && typeof project.userId === 'string' ? project.userId.trim() : null
if (!uid) {
return null;
return null
}
return creatorsById?.[uid]?.premiumLevel || null;
return creatorsById?.[uid]?.premiumLevel || null
},
[creatorsById]
);
)
const playbackResults = useMemo(
() => (Array.isArray(playbacks) ? playbacks : []),
[playbacks]
);
const playbackResults = useMemo(() => (Array.isArray(playbacks) ? playbacks : []), [playbacks])
const userResults = useMemo(
() => (Array.isArray(users) ? users : []),
[users]
);
const userResults = useMemo(() => (Array.isArray(users) ? users : []), [users])
const musicsLoading = searchLoading;
const playbacksLoading = searchLoading;
const usersLoading = searchLoading;
const musicsLoading = searchLoading
const playbacksLoading = searchLoading
const usersLoading = searchLoading
const closeDropdown = useCallback(() => {
setDropdownVisible(false);
setSearchSelected(null);
setSearch("");
}, [setSearch, setSearchSelected]);
setDropdownVisible(false)
setSearchSelected(null)
setSearch('')
}, [setSearch, setSearchSelected])
const handleSearchFocus = useCallback(() => {
if (isWeb) {
setDropdownVisible(true);
setDropdownVisible(true)
}
}, [isWeb]);
}, [isWeb])
const handleSearchChange = useCallback(
(value) => {
setSearch(value);
setSearch(value)
if (isWeb) {
setDropdownVisible(true);
setDropdownVisible(true)
}
},
[isWeb, setSearch]
);
)
useEffect(() => {
if (!isWeb || !dropdownVisible) {
return undefined;
return undefined
}
const handleClickOutside = (event) => {
if (searchWrapperRef.current?.contains?.(event.target)) {
return;
return
}
closeDropdown();
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [closeDropdown, dropdownVisible, isWeb]);
const shouldShowResults = isWeb && dropdownVisible;
const shouldBlurContent = shouldShowResults;
const handlePlayRandomSong = useCallback(() => {
const arr = Array.isArray(topSongs) ? topSongs : [];
if (!arr.length) {
console.warn("Aucune chanson disponible pour le moment.");
return;
closeDropdown()
}
const randomSong = arr[Math.floor(Math.random() * arr.length)];
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
}, [closeDropdown, dropdownVisible, isWeb])
const shouldShowResults = isWeb && dropdownVisible
const shouldBlurContent = shouldShowResults
const handlePlayRandomSong = useCallback(() => {
const arr = Array.isArray(topSongs) ? topSongs : []
if (!arr.length) {
console.warn('Aucune chanson disponible pour le moment.')
return
}
const randomSong = arr[Math.floor(Math.random() * arr.length)]
navigateToMusicDetails({
projectId: randomSong.id,
songUrl: randomSong?.songUrl,
project: randomSong,
});
}, [navigateToMusicDetails, topSongs]);
})
}, [navigateToMusicDetails, topSongs])
const categoryLabel =
selectedCategory === "Playback" ? "playbacks" : "chansons";
const categoryLabel = selectedCategory === 'Playback' ? 'playbacks' : 'chansons'
const heroTitle = isWeb
? "Coups de coeur des utilisateurs"
: `Coups de coeur des utilisateurs catégorie ${selectedCategory}`;
? 'Coups de coeur des utilisateurs'
: `Coups de coeur des utilisateurs catégorie ${selectedCategory}`
const heroSubtitle = isWeb
? "Les 3 chansons et playbacks les plus likées du mois reçoivent des tokens."
: `Les 3 ${categoryLabel} les plus likées du mois reçoivent des tokens.`;
? 'Les 3 chansons et playbacks les plus likées du mois reçoivent des tokens.'
: `Les 3 ${categoryLabel} les plus likées du mois reçoivent des tokens.`
const renderHero = () => (
<View
style={{ gap: 11, maxWidth: 800, width: "100%", alignSelf: "center" }}
>
<View style={{ gap: 11, maxWidth: 800, width: '100%', alignSelf: 'center' }}>
{/* <Pressable onPress={handlePlayRandomSong}>
<Image
source={icons.play}
style={{ alignSelf: "center", marginVertical: 10 }}
/>
</Pressable> */}
<CreateLyricsHeader gradientProps={{ colors: ["#F94697", "#7023F7"] }}>
<CreateLyricsHeader gradientProps={{ colors: ['#F94697', '#7023F7'] }}>
<Text
style={{
fontSize: 16,
@@ -423,7 +387,7 @@ const HitParade = () => {
</Text>
</CreateLyricsHeader>
</View>
);
)
const renderWebContent = () => (
<ScrollView
@@ -433,14 +397,14 @@ const HitParade = () => {
paddingBottom: responsiveHeight(20),
}}
showsVerticalScrollIndicator={false}
style={{ flex: 1, position: "relative", zIndex: 5 }}
style={{ flex: 1, position: 'relative', zIndex: 5 }}
>
{renderHero()}
<View
style={{
flexDirection: "row",
flexDirection: 'row',
gap: 16,
alignItems: "flex-start",
alignItems: 'flex-start',
}}
>
<View style={{ flex: 1, gap: 12 }}>
@@ -448,9 +412,9 @@ const HitParade = () => {
intensity={50}
tint="dark"
style={{
alignSelf: "flex-start",
alignSelf: 'flex-start',
borderRadius: 10,
overflow: "hidden",
overflow: 'hidden',
paddingHorizontal: 20,
paddingVertical: 10,
}}
@@ -474,9 +438,9 @@ const HitParade = () => {
intensity={50}
tint="dark"
style={{
alignSelf: "flex-start",
alignSelf: 'flex-start',
borderRadius: 10,
overflow: "hidden",
overflow: 'hidden',
paddingHorizontal: 20,
paddingVertical: 10,
}}
@@ -497,7 +461,7 @@ const HitParade = () => {
</View>
</View>
</ScrollView>
);
)
const renderMobileContent = () => (
<View style={{ flex: 1, gap: 24 }}>
@@ -505,17 +469,17 @@ const HitParade = () => {
<View
style={{
...Style.containerRow,
justifyContent: "space-between",
width: "auto",
justifyContent: 'space-between',
width: 'auto',
gap: 12,
}}
>
{["Chansons", "Playback"].map((item, index) => (
{['Chansons', 'Playback'].map((item, index) => (
<BorderGradientButton
key={index}
title={item}
onPress={() => setSelectedCategory(item)}
tint={selectedCategory === item ? "light" : "dark"}
tint={selectedCategory === item ? 'light' : 'dark'}
containerStyle={{ flex: 1 }}
titleStyle={{
fontSize: 16,
@@ -525,11 +489,11 @@ const HitParade = () => {
))}
</View>
{selectedCategory === "Chansons" && renderSongsMobile()}
{selectedCategory === "Playback" && renderPlaybacksMobile()}
{selectedCategory === 'Chansons' && renderSongsMobile()}
{selectedCategory === 'Playback' && renderPlaybacksMobile()}
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
</View>
);
)
return (
<Page
@@ -543,10 +507,10 @@ const HitParade = () => {
{!isWeb ? (
<View
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
marginBottom: 4,
}}
@@ -558,20 +522,20 @@ const HitParade = () => {
<View style={{ gap: 12, flex: 1 }}>
<Image
source={icons.musicLandLogo}
style={{ alignSelf: "center", height: 150, resizeMode: "contain" }}
style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }}
/>
{isWeb && (
<View
ref={searchWrapperRef}
style={{
position: "relative",
alignSelf: "center",
width: "65%",
position: 'relative',
alignSelf: 'center',
width: '65%',
maxWidth: 520,
minWidth: 360,
zIndex: dropdownVisible ? 40 : 1,
overflow: "visible",
overflow: 'visible',
}}
>
<SearchBar
@@ -585,12 +549,12 @@ const HitParade = () => {
{shouldShowResults && (
<View
style={{
position: "absolute",
position: 'absolute',
top: 60,
left: 0,
right: 0,
zIndex: 50,
width: "100%",
width: '100%',
}}
>
<View
@@ -600,7 +564,7 @@ const HitParade = () => {
padding: 16,
backgroundColor: Palette.ultraLightWhite,
...Style.defaultBorder,
width: "100%",
width: '100%',
zIndex: 50,
elevation: 12,
}}
@@ -627,7 +591,7 @@ const HitParade = () => {
</View>
)}
<View style={{ flex: 1, position: "relative" }}>
<View style={{ flex: 1, position: 'relative' }}>
{shouldBlurContent && (
<BlurView
intensity={35}
@@ -637,8 +601,8 @@ const HitParade = () => {
{
zIndex: 10,
borderRadius: 18,
backgroundColor: "rgba(0, 0, 0, 0.25)",
overflow: "hidden",
backgroundColor: 'rgba(0, 0, 0, 0.25)',
overflow: 'hidden',
},
]}
/>
@@ -648,7 +612,7 @@ const HitParade = () => {
</View>
</View>
</Page>
);
};
)
}
export default HitParade;
export default HitParade
@@ -1,28 +1,28 @@
import { View, Text, Image, Platform, Pressable } from "react-native";
import React from "react";
import { BlurView } from "expo-blur";
import { img } from "../../../assets";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import SubscriptionBadge from "../../../components/SubscriptionBadge";
import { View, Text, Image, Platform, Pressable } from 'react-native'
import React from 'react'
import { BlurView } from 'expo-blur'
import { img } from '../../../assets'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import SubscriptionBadge from '../../../components/SubscriptionBadge'
const PlaybacksCard = ({
rank = 1,
title = "Sans titre",
artist = "MusicLand",
title = 'Sans titre',
artist = 'MusicLand',
thumbnailUrl = null,
coverUrl = null,
onPress = () => null,
subscriptionLevel = null,
}) => {
const resolveUri = (value) =>
typeof value === "string" && value.trim().length > 0 ? value : null;
const imageUri = resolveUri(thumbnailUrl) ?? resolveUri(coverUrl);
typeof value === 'string' && value.trim().length > 0 ? value : null
const imageUri = resolveUri(thumbnailUrl) ?? resolveUri(coverUrl)
return (
<Pressable onPress={onPress}>
<BlurView
intensity={Platform.OS === "web" ? 40 : Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS === 'web' ? 40 : Platform.OS !== 'ios' ? 10 : 20}
tint="dark"
style={{
...Style.containerSpaceBetween,
@@ -30,7 +30,7 @@ const PlaybacksCard = ({
paddingVertical: 4,
borderRadius: 12,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -40,16 +40,16 @@ const PlaybacksCard = ({
style={{
...Style.containerRow,
gap: 15,
alignItems: "center",
justifyContent: "space-between",
width: "100%",
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<View
style={{
...Style.containerRow,
gap: 15,
alignItems: "center",
alignItems: 'center',
flex: 1,
}}
>
@@ -81,9 +81,9 @@ const PlaybacksCard = ({
style={{
...Style.containerRow,
gap: 6,
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Text
@@ -104,7 +104,7 @@ const PlaybacksCard = ({
</View>
</BlurView>
</Pressable>
);
};
)
}
export default PlaybacksCard;
export default PlaybacksCard
+21 -34
View File
@@ -1,23 +1,17 @@
import {
View,
Text,
Platform,
Pressable,
Image as RNImage,
} from "react-native";
import { Image as ExpoImage } from "expo-image";
import React from "react";
import { img } from "../../../assets";
import { BlurView } from "expo-blur";
import Style, { size } from "../../../styles/Style";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import SubscriptionBadge from "../../../components/SubscriptionBadge";
import { View, Text, Platform, Pressable, Image as RNImage } from 'react-native'
import { Image as ExpoImage } from 'expo-image'
import React from 'react'
import { img } from '../../../assets'
import { BlurView } from 'expo-blur'
import Style, { size } from '../../../styles/Style'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import SubscriptionBadge from '../../../components/SubscriptionBadge'
const SongCard = ({
rank = 1,
title = "Sans titre",
artist = "MusicLand",
title = 'Sans titre',
artist = 'MusicLand',
coverUrl = null,
onPress = null,
subscriptionLevel = null,
@@ -40,17 +34,14 @@ const SongCard = ({
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
<RNImage source={img.placeholder2} style={{ ...size({ size: 60 }), borderRadius: 12 }} />
)}
<BlurView
intensity={Platform.OS === "web" ? 40 : Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS === 'web' ? 40 : Platform.OS !== 'ios' ? 10 : 20}
style={{
flex: 1,
borderRadius: 12,
overflow: "hidden",
overflow: 'hidden',
paddingHorizontal: 8,
paddingVertical: 11,
backgroundColor: Palette.glass,
@@ -85,9 +76,9 @@ const SongCard = ({
style={{
...Style.containerRow,
gap: 6,
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Text
@@ -102,15 +93,11 @@ const SongCard = ({
</Text>
</View>
</View>
<SubscriptionBadge
level={subscriptionLevel}
size={36}
style={{ marginLeft: "auto" }}
/>
<SubscriptionBadge level={subscriptionLevel} size={36} style={{ marginLeft: 'auto' }} />
</View>
</BlurView>
</Pressable>
);
};
)
}
export default SongCard;
export default SongCard
+305 -353
View File
File diff suppressed because it is too large Load Diff
+20 -24
View File
@@ -1,20 +1,16 @@
import React, { memo } from "react";
import { Image, Pressable, StyleSheet } from "react-native";
import { isWeb } from "../../../hooks/useLayoutType.js";
import { img } from "../../../assets/index.js";
import React, { memo } from 'react'
import { Image, Pressable, StyleSheet } from 'react-native'
import { isWeb } from '../../../hooks/useLayoutType.js'
import { img } from '../../../assets/index.js'
const ClubCard = ({
onPress,
containerStyle = null,
variant = "compact",
}) => {
const isStageVariant = variant === "stage";
const ClubCard = ({ onPress, containerStyle = null, variant = 'compact' }) => {
const isStageVariant = variant === 'stage'
const cardBaseStyle = isStageVariant
? isWeb
? styles.stageCardWeb
: styles.stageCardMobile
: styles.compactCard;
const imageStyle = isStageVariant ? styles.stageImage : styles.compactImage;
: styles.compactCard
const imageStyle = isStageVariant ? styles.stageImage : styles.compactImage
return (
<Pressable
@@ -28,26 +24,26 @@ const ClubCard = ({
>
<Image source={img.musiclandClub} style={imageStyle} />
</Pressable>
);
};
)
}
export default ClubCard;
export default ClubCard
const styles = StyleSheet.create({
card: {},
compactCard: {
marginTop: isWeb ? 16 : 28,
alignSelf: "center",
alignSelf: 'center',
},
stageCardWeb: {
width: "48%",
width: '48%',
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
},
stageCardMobile: {
width: "100%",
width: '100%',
borderRadius: 24,
overflow: "hidden",
overflow: 'hidden',
marginBottom: 20,
},
cardPressed: {
@@ -56,11 +52,11 @@ const styles = StyleSheet.create({
compactImage: {
width: 210,
height: 120,
resizeMode: "contain",
resizeMode: 'contain',
},
stageImage: {
width: "100%",
width: '100%',
height: isWeb ? 210 : 300,
resizeMode: "contain",
resizeMode: 'contain',
},
});
})
+52 -60
View File
@@ -1,44 +1,44 @@
import React, { memo } from "react";
import { Pressable, Text, View } from "react-native";
import { BlurView } from "expo-blur";
import { Image as ExpoImage } from "expo-image";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import React, { memo } from 'react'
import { Pressable, Text, View } from 'react-native'
import { BlurView } from 'expo-blur'
import { Image as ExpoImage } from 'expo-image'
import FontAwesome from '@expo/vector-icons/FontAwesome'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
const StageCard = ({
step,
description,
image,
imagePosition = "left",
textAlign = "left",
imagePosition = 'left',
textAlign = 'left',
isLocked,
onPress,
containerStyle = null,
variant = "web",
variant = 'web',
}) => {
const isMobileVariant = variant === "mobile";
const isImageOnLeft = !isMobileVariant && imagePosition !== "right";
const isMobileVariant = variant === 'mobile'
const isImageOnLeft = !isMobileVariant && imagePosition !== 'right'
const basePressableStyle =
variant === "web"
? { width: "48%" }
variant === 'web'
? { width: '48%' }
: {
width: "100%",
width: '100%',
borderRadius: 24,
overflow: "hidden",
};
overflow: 'hidden',
}
const imageContainerBaseStyle = {
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
zIndex: 10,
};
}
const imageContainerVariantStyle = isMobileVariant
? {
width: "100%",
width: '100%',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
borderBottomLeftRadius: 0,
@@ -60,7 +60,7 @@ const StageCard = ({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}),
};
}
const imageBlock = (
<View style={[imageContainerBaseStyle, imageContainerVariantStyle]}>
@@ -69,33 +69,31 @@ const StageCard = ({
contentFit="contain"
style={[
{
width: "100%",
width: '100%',
height: 190,
minHeight: 210,
borderRadius: 0,
},
isMobileVariant
? { alignSelf: "center" }
? { alignSelf: 'center' }
: isImageOnLeft
? { alignSelf: "flex-end" }
: { alignSelf: "flex-start" },
? { alignSelf: 'flex-end' }
: { alignSelf: 'flex-start' },
]}
/>
</View>
);
)
const header = (
<View
style={[
{
width: "100%",
flexDirection: "row",
alignItems: "center",
width: '100%',
flexDirection: 'row',
alignItems: 'center',
marginBottom: isMobileVariant ? 8 : 6,
},
isMobileVariant
? { justifyContent: "center" }
: { justifyContent: "flex-start" },
isMobileVariant ? { justifyContent: 'center' } : { justifyContent: 'flex-start' },
]}
>
<Text
@@ -106,30 +104,30 @@ const StageCard = ({
color: Palette.white,
flexShrink: 1,
},
isMobileVariant ? { textAlign: "center" } : { textAlign: "left" },
isMobileVariant ? { textAlign: 'center' } : { textAlign: 'left' },
]}
numberOfLines={1}
>
{step}
</Text>
</View>
);
)
const textBlockBaseStyle = {
flexGrow: 1,
flexShrink: 1,
paddingVertical: isMobileVariant ? 20 : 16,
paddingHorizontal: 20,
justifyContent: "center",
alignSelf: "center",
};
justifyContent: 'center',
alignSelf: 'center',
}
const textBlockVariantStyle = isMobileVariant
? {
width: "100%",
width: '100%',
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
alignItems: "center",
alignItems: 'center',
paddingVertical: 20,
paddingHorizontal: 20,
marginTop: 0,
@@ -146,26 +144,22 @@ const StageCard = ({
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
}),
};
}
const textBlock = (
<BlurView
tint="dark"
intensity={30}
style={[textBlockBaseStyle, textBlockVariantStyle]}
>
<BlurView tint="dark" intensity={30} style={[textBlockBaseStyle, textBlockVariantStyle]}>
{isLocked ? (
<View
style={[
{
position: "absolute",
position: 'absolute',
top: 10,
backgroundColor: "rgba(0, 0, 0, 0.55)",
backgroundColor: 'rgba(0, 0, 0, 0.55)',
width: 32,
height: 32,
borderRadius: 16,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
zIndex: 2,
right: 10,
},
@@ -177,9 +171,7 @@ const StageCard = ({
{header}
<Text
style={[
isMobileVariant
? { textAlign: "center" }
: { textAlign: "left", marginRight: 10 },
isMobileVariant ? { textAlign: 'center' } : { textAlign: 'left', marginRight: 10 },
{
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
@@ -191,7 +183,7 @@ const StageCard = ({
{description}
</Text>
</BlurView>
);
)
const content = isMobileVariant ? (
<>
@@ -208,7 +200,7 @@ const StageCard = ({
{textBlock}
{imageBlock}
</>
);
)
return (
<Pressable
@@ -222,13 +214,13 @@ const StageCard = ({
>
<View
style={{
flexDirection: isMobileVariant ? "column" : "row",
flexDirection: isMobileVariant ? 'column' : 'row',
}}
>
{content}
</View>
</Pressable>
);
};
)
}
export default memo(StageCard);
export default memo(StageCard)
+69 -91
View File
@@ -1,89 +1,82 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useNavigation } from "@react-navigation/native";
import { useCallback, useEffect, useState } from "react";
import {
Image,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import { background, icons } from "../assets";
import FullscreenIntroVideo from "../components/FullscreenIntroVideo";
import GradientButton from "../components/GradientButton";
import { videosRef } from "../config/firebase";
import useDataFromRef from "../hooks/useDataFromRef";
import Page from "../layouts/Page";
import { Routes } from "../navigation";
import { useUser } from "../providers/UserDataProvider";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { BlurView } from "expo-blur";
import AsyncStorage from '@react-native-async-storage/async-storage'
import { useNavigation } from '@react-navigation/native'
import { useCallback, useEffect, useState } from 'react'
import { Image, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { background, icons } from '../assets'
import FullscreenIntroVideo from '../components/FullscreenIntroVideo'
import GradientButton from '../components/GradientButton'
import { videosRef } from '../config/firebase'
import useDataFromRef from '../hooks/useDataFromRef'
import Page from '../layouts/Page'
import { Routes } from '../navigation'
import { useUser } from '../providers/UserDataProvider'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { BlurView } from 'expo-blur'
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
const LANGUAGE_STORAGE_KEY = 'preferredLanguage'
export default function LandingPage() {
const navigation = useNavigation();
const { currentUID } = useUser();
const [videoUrl, setVideoUrl] = useState(null);
const [selectedLanguage, setSelectedLanguage] = useState(null);
const [showFreeCreditsPopup, setShowFreeCreditsPopup] = useState(false);
const isAuthenticated = !!currentUID;
const navigation = useNavigation()
const { currentUID } = useUser()
const [videoUrl, setVideoUrl] = useState(null)
const [selectedLanguage, setSelectedLanguage] = useState(null)
const [showFreeCreditsPopup, setShowFreeCreditsPopup] = useState(false)
const isAuthenticated = !!currentUID
const handleSelectLanguage = useCallback((language) => {
setSelectedLanguage(language);
setSelectedLanguage(language)
AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, language).catch((error) => {
console.warn("LandingPage: failed to persist language", error);
});
}, []);
console.warn('LandingPage: failed to persist language', error)
})
}, [])
const handleLaunchAdventure = useCallback(() => {
if (isAuthenticated) {
navigation.navigate(Routes.BottomTab, {
screen: Routes.HomeStack,
params: { screen: Routes.Home },
});
return;
})
return
}
navigation.navigate(Routes.Register);
}, [isAuthenticated, navigation]);
navigation.navigate(Routes.Register)
}, [isAuthenticated, navigation])
const handleOpenSubscriptions = useCallback(() => {
navigation.navigate(Routes.Payments);
}, [navigation]);
navigation.navigate(Routes.Payments)
}, [navigation])
const { data: video } = useDataFromRef({
ref: videosRef.doc("fr"),
ref: videosRef.doc('fr'),
simpleRef: true,
});
})
useEffect(() => {
let isMounted = true;
let isMounted = true
AsyncStorage.getItem(LANGUAGE_STORAGE_KEY)
.then((storedLanguage) => {
if (storedLanguage && isMounted) {
setSelectedLanguage(storedLanguage);
setSelectedLanguage(storedLanguage)
}
})
.catch((error) => {
console.warn("LandingPage: failed to load language", error);
});
console.warn('LandingPage: failed to load language', error)
})
return () => {
isMounted = false;
};
}, []);
isMounted = false
}
}, [])
useEffect(() => {
if (currentUID) {
setShowFreeCreditsPopup(false);
return;
setShowFreeCreditsPopup(false)
return
}
if (selectedLanguage) {
setShowFreeCreditsPopup(true);
setShowFreeCreditsPopup(true)
}
}, [currentUID, selectedLanguage]);
}, [currentUID, selectedLanguage])
return (
<>
@@ -96,8 +89,8 @@ export default function LandingPage() {
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<View style={styles.content}>
@@ -105,13 +98,13 @@ export default function LandingPage() {
<View style={styles.languageRow}>
<TouchableOpacity
style={[styles.languageButton]}
onPress={() => handleSelectLanguage("fr")}
onPress={() => handleSelectLanguage('fr')}
>
<Text style={styles.languageText}>🇫🇷 Français</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.languageButton]}
onPress={() => handleSelectLanguage("en")}
onPress={() => handleSelectLanguage('en')}
>
<Text style={styles.languageText}>🇬🇧 English</Text>
</TouchableOpacity>
@@ -127,11 +120,7 @@ export default function LandingPage() {
<GradientButton
title="Présentation de Musicland"
onPress={() =>
setVideoUrl(
Platform.OS === "web"
? video?.landingWeb
: video?.landing,
)
setVideoUrl(Platform.OS === 'web' ? video?.landingWeb : video?.landing)
}
containerStyle={styles.actionButton}
/>
@@ -139,9 +128,9 @@ export default function LandingPage() {
title={
isAuthenticated
? "Continuer l'aventure"
: Platform.OS === "web"
: Platform.OS === 'web'
? "Je me lance dans l'aventure"
: "Commencer"
: 'Commencer'
}
onPress={handleLaunchAdventure}
containerStyle={styles.actionButton}
@@ -157,12 +146,8 @@ export default function LandingPage() {
<BlurView style={styles.freeCreditsCard}>
<View style={styles.freeCreditsContent}>
<View style={styles.freeCreditsTextContainer}>
<Text style={styles.freeCreditsTitle}>
10 crédits offert
</Text>
<Text style={styles.freeCreditsSubtitle}>
pour 1 parcours gratuit !
</Text>
<Text style={styles.freeCreditsTitle}>10 crédits offert</Text>
<Text style={styles.freeCreditsSubtitle}>pour 1 parcours gratuit !</Text>
</View>
<TouchableOpacity
onPress={() => setShowFreeCreditsPopup(false)}
@@ -170,10 +155,7 @@ export default function LandingPage() {
accessibilityLabel="Fermer le message des crédits offerts"
style={styles.freeCreditsClose}
>
<Image
source={icons.close}
style={styles.freeCreditsCloseIcon}
/>
<Image source={icons.close} style={styles.freeCreditsCloseIcon} />
</TouchableOpacity>
</View>
</BlurView>
@@ -183,26 +165,22 @@ export default function LandingPage() {
</View>
</View>
</Page>
<FullscreenIntroVideo
url={videoUrl}
visible={!!videoUrl}
onClose={() => setVideoUrl(null)}
/>
<FullscreenIntroVideo url={videoUrl} visible={!!videoUrl} onClose={() => setVideoUrl(null)} />
</>
);
)
}
const styles = StyleSheet.create({
content: {
width: "100%",
width: '100%',
maxWidth: 360,
alignItems: "center",
alignItems: 'center',
gap: 32,
},
languageRow: {
flexDirection: "row",
flexDirection: 'row',
gap: 16,
justifyContent: "center",
justifyContent: 'center',
},
languageButton: {
borderRadius: 14,
@@ -212,7 +190,7 @@ const styles = StyleSheet.create({
paddingVertical: 12,
paddingHorizontal: 18,
minWidth: 140,
alignItems: "center",
alignItems: 'center',
},
languageText: {
fontSize: 16,
@@ -220,23 +198,23 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterSemiBold,
},
actions: {
width: "100%",
width: '100%',
maxWidth: 320,
gap: 16,
},
actionButton: {
width: "100%",
width: '100%',
},
freeCreditsCard: {
width: "100%",
width: '100%',
borderRadius: 18,
paddingVertical: 14,
paddingHorizontal: 20,
overflow: "hidden"
overflow: 'hidden',
},
freeCreditsContent: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
freeCreditsTextContainer: {
@@ -262,4 +240,4 @@ const styles = StyleSheet.create({
height: 14,
tintColor: Palette.grayMid,
},
});
})
+71 -100
View File
@@ -1,26 +1,22 @@
import { useRoute } from "@react-navigation/native";
import React, { useMemo, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import MusicCard from "./components/MusicCard";
import { useRoute } from '@react-navigation/native'
import React, { useMemo, useState } from 'react'
import { ActivityIndicator, Text, View } from 'react-native'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import MoreMenu from '../../components/MoreMenu'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette, Style, gutters } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { getProjectLikes, LIKE_TARGET } from '../../utils/likes'
import MusicCard from './components/MusicCard'
export default function AllMyList() {
const route = useRoute();
const {
title = "Mes éléments",
scope = "music",
liked = false,
} = route?.params || {};
const route = useRoute()
const { title = 'Mes éléments', scope = 'music', liked = false } = route?.params || {}
const {
userProjects,
@@ -30,102 +26,85 @@ export default function AllMyList() {
userPlaybacks,
userLikedPlaybacks,
userLikedPlaybacksLoading,
} = useUser();
const navigateToMusicDetails = useNavigateToMusicDetails();
} = useUser()
const navigateToMusicDetails = useNavigateToMusicDetails()
const items = useMemo(() => {
if (scope === "playback") {
if (scope === 'playback') {
return Array.isArray(liked ? userLikedPlaybacks : userPlaybacks)
? liked
? userLikedPlaybacks
: userPlaybacks
: [];
: []
}
// music (projects)
return Array.isArray(liked ? userLikedProjects : userProjects)
? liked
? userLikedProjects
: userProjects
: [];
}, [
liked,
scope,
userLikedPlaybacks,
userPlaybacks,
userLikedProjects,
userProjects,
]);
: []
}, [liked, scope, userLikedPlaybacks, userPlaybacks, userLikedProjects, userProjects])
const sanitizedItems = Array.isArray(items) ? items : [];
const sanitizedItems = Array.isArray(items) ? items : []
const itemsLoading = useMemo(() => {
if (scope === "playback") {
return liked ? userLikedPlaybacksLoading : userProjectsLoading;
if (scope === 'playback') {
return liked ? userLikedPlaybacksLoading : userProjectsLoading
}
return liked ? userLikedProjectsLoading : userProjectsLoading;
}, [
liked,
scope,
userLikedPlaybacksLoading,
userLikedProjectsLoading,
userProjectsLoading,
]);
return liked ? userLikedProjectsLoading : userProjectsLoading
}, [liked, scope, userLikedPlaybacksLoading, userLikedProjectsLoading, userProjectsLoading])
const emptyStateContent = useMemo(() => {
const variant = liked ? "liked" : "default";
const scopeKey = scope === "playback" ? "playback" : "music";
const variant = liked ? 'liked' : 'default'
const scopeKey = scope === 'playback' ? 'playback' : 'music'
const content = {
music: {
default: {
title: "Aucune musique pour le moment",
title: 'Aucune musique pour le moment',
description:
"Créez un nouveau titre ou explorez la bibliothèque pour alimenter votre collection.",
'Créez un nouveau titre ou explorez la bibliothèque pour alimenter votre collection.',
cta: {
label: "Créer une musique",
label: 'Créer une musique',
action: () => navigate(Routes.WritingLyrics),
},
},
liked: {
title: "Aucune musique likée pour le moment",
description:
"Likez les morceaux qui vous inspirent pour les garder à portée de main.",
title: 'Aucune musique likée pour le moment',
description: 'Likez les morceaux qui vous inspirent pour les garder à portée de main.',
cta: {
label: "Explorer la bibliothèque",
label: 'Explorer la bibliothèque',
action: () => navigate(Routes.Research),
},
},
},
playback: {
default: {
title: "Aucun playback pour le moment",
description:
"Enregistrez ou importez un playback pour le retrouver facilement ici.",
title: 'Aucun playback pour le moment',
description: 'Enregistrez ou importez un playback pour le retrouver facilement ici.',
cta: {
label: "Créer un playback",
label: 'Créer un playback',
action: () => navigate(Routes.PlaybackOnboarding),
},
},
liked: {
title: "Aucun playback liké pour le moment",
title: 'Aucun playback liké pour le moment',
description:
"Ajoutez vos playback favoris à vos likes pour les retrouver instantanément.",
'Ajoutez vos playback favoris à vos likes pour les retrouver instantanément.',
cta: {
label: "Voir les playback",
label: 'Voir les playback',
action: () => navigate(Routes.Playbacks),
},
},
},
};
}
return content[scopeKey][variant];
}, [liked, scope]);
return content[scopeKey][variant]
}, [liked, scope])
const loadingMessage = useMemo(() => {
const target = scope === "playback" ? "playback" : "musiques";
return liked
? `Chargement des ${target} likés…`
: `Chargement des ${target}…`;
}, [liked, scope]);
const target = scope === 'playback' ? 'playback' : 'musiques'
return liked ? `Chargement des ${target} likés…` : `Chargement des ${target}…`
}, [liked, scope])
const placeholderBaseStyle = {
gap: 16,
@@ -133,40 +112,40 @@ export default function AllMyList() {
paddingHorizontal: 32,
backgroundColor: Palette.glass,
borderRadius: 16,
alignSelf: "stretch",
};
alignSelf: 'stretch',
}
const placeholderTitleStyle = {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
};
textAlign: 'center',
}
const placeholderDescriptionStyle = {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
textAlign: "center",
textAlign: 'center',
maxWidth: 420,
};
}
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const onPressItem = (item) => {
if (scope === "playback") {
navigate(Routes.Playbacks, { projectId: item.id });
if (scope === 'playback') {
navigate(Routes.Playbacks, { projectId: item.id })
} else {
navigateToMusicDetails({
projectId: item.id,
songUrl: item?.songUrl,
project: item,
});
})
}
};
}
return (
<Page
@@ -184,12 +163,11 @@ export default function AllMyList() {
</View>
) : sanitizedItems.length > 0 ? (
sanitizedItems.map((item) => {
const likeTarget =
scope === "playback" ? LIKE_TARGET.PLAYBACK : LIKE_TARGET.SONG;
const likeTarget = scope === 'playback' ? LIKE_TARGET.PLAYBACK : LIKE_TARGET.SONG
return (
<MusicCard
key={item.id}
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
imageUri={item?.coverUrl || null}
subtitle={item?.userName}
projectId={item?.id}
@@ -197,30 +175,23 @@ export default function AllMyList() {
likeTarget={likeTarget}
onPress={() => onPressItem(item)}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuPosition(posTop);
setShowMenu(
(prev) =>
!prev || posTop?.top !== (menuPosition?.top ?? null)
);
setSelectedProjectId(item.id)
setMenuPosition(posTop)
setShowMenu((prev) => !prev || posTop?.top !== (menuPosition?.top ?? null))
}}
/>
);
)
})
) : (
<View style={[Style.containerCenter, placeholderBaseStyle]}>
<Text style={placeholderTitleStyle}>
{emptyStateContent.title}
</Text>
<Text style={placeholderDescriptionStyle}>
{emptyStateContent.description}
</Text>
<Text style={placeholderTitleStyle}>{emptyStateContent.title}</Text>
<Text style={placeholderDescriptionStyle}>{emptyStateContent.description}</Text>
{emptyStateContent?.cta ? (
<GradientButton
title={emptyStateContent.cta.label}
onPress={emptyStateContent.cta.action}
containerStyle={{
alignSelf: "center",
alignSelf: 'center',
minWidth: 200,
}}
/>
@@ -238,5 +209,5 @@ export default function AllMyList() {
</View>
</View>
</Page>
);
)
}
+23 -31
View File
@@ -1,17 +1,17 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { background, icons } from "../../assets";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Platform, Pressable, Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { background, icons } from '../../assets'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { size } from '../../styles/Style'
const AllMyPlaylist = () => {
const { userPlaylists = [] } = useUser();
const { userPlaylists = [] } = useUser()
return (
<Page
headerType="NAVIGATION"
@@ -19,15 +19,9 @@ const AllMyPlaylist = () => {
title="Mes Playlists"
rightComponent={() => (
<Pressable
onPress={() =>
SheetManager.show("Playlist", { payload: { startAtCreate: true } })
}
onPress={() => SheetManager.show('Playlist', { payload: { startAtCreate: true } })}
>
<Image
source={icons.add}
style={size({ size: 20 })}
resizeMode="contain"
/>
<Image source={icons.add} style={size({ size: 20 })} resizeMode="contain" />
</Pressable>
)}
contentContainerStyle={{
@@ -39,13 +33,11 @@ const AllMyPlaylist = () => {
{(Array.isArray(userPlaylists) ? userPlaylists : []).map((item) => (
<Pressable
key={item?.id}
onPress={() =>
navigate(Routes.PlaylistDetails, { playlistId: item?.id })
}
onPress={() => navigate(Routes.PlaylistDetails, { playlistId: item?.id })}
>
<View style={{ borderRadius: 12, overflow: "hidden" }}>
<View style={{ borderRadius: 12, overflow: 'hidden' }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
...Style.containerSpaceBetween,
minHeight: 59,
@@ -66,14 +58,14 @@ const AllMyPlaylist = () => {
flexShrink: 1,
}}
>
{item?.name || "Sans nom"}
{item?.name || 'Sans nom'}
</Text>
<Pressable>
<Image
source={icons.chevronDown}
style={{
...size({ size: 15 }),
transform: [{ rotate: "-90deg" }],
transform: [{ rotate: '-90deg' }],
}}
resizeMode="contain"
/>
@@ -85,7 +77,7 @@ const AllMyPlaylist = () => {
</View>
</View>
</Page>
);
};
)
}
export default AllMyPlaylist;
export default AllMyPlaylist
+113 -133
View File
@@ -1,37 +1,35 @@
import { BlurView } from "expo-blur";
import React, { useCallback, useMemo, useState } from "react";
import { Image, Pressable, Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useGlobal } from "reactn";
import { background, icons } from "../../assets";
import MoreMenu from "../../components/MoreMenu";
import { playlistsRef, projectsRef } from "../../config/firebase";
import useDataFromArrayDocId from "../../hooks/useDataFromArrayId";
import useDataFromRef from "../../hooks/useDataFromRef";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import usePlayer from "../../hooks/usePlayer";
import Page from "../../layouts/Page";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import MusicCard from "./components/MusicCard";
import { BlurView } from 'expo-blur'
import React, { useCallback, useMemo, useState } from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { useGlobal } from 'reactn'
import { background, icons } from '../../assets'
import MoreMenu from '../../components/MoreMenu'
import { playlistsRef, projectsRef } from '../../config/firebase'
import useDataFromArrayDocId from '../../hooks/useDataFromArrayId'
import useDataFromRef from '../../hooks/useDataFromRef'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import usePlayer from '../../hooks/usePlayer'
import Page from '../../layouts/Page'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { size } from '../../styles/Style'
import { getProjectLikes, LIKE_TARGET } from '../../utils/likes'
import MusicCard from './components/MusicCard'
const AllMyPlaylist = ({ route }) => {
const { playListId } = route?.params || {};
const [, setTooltip] = useGlobal("_tooltip");
const { userPlaylists = [] } = useUser();
const [selectedPlaylistId, setSelectedPlaylistId] = useState(
playListId || null
);
const { playListId } = route?.params || {}
const [, setTooltip] = useGlobal('_tooltip')
const { userPlaylists = [] } = useUser()
const [selectedPlaylistId, setSelectedPlaylistId] = useState(playListId || null)
const { data: playlist } = useDataFromRef({
ref: selectedPlaylistId ? playlistsRef.doc(selectedPlaylistId) : null,
simpleRef: true,
listener: true,
condition: !!selectedPlaylistId,
refreshArray: [selectedPlaylistId],
});
})
// Fetch projects by ids from playlist.musics
const { data: musics } = useDataFromArrayDocId({
@@ -39,31 +37,31 @@ const AllMyPlaylist = ({ route }) => {
arrayId: Array.isArray(playlist?.musics) ? playlist.musics : [],
condition: Array.isArray(playlist?.musics) && playlist.musics.length > 0,
refreshArray: [selectedPlaylistId, playlist?.musics?.length || 0],
});
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const navigateToMusicDetails = useNavigateToMusicDetails();
const { setQueue } = usePlayer() || {};
})
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const navigateToMusicDetails = useNavigateToMusicDetails()
const { setQueue } = usePlayer() || {}
const playlistQueueItems = useMemo(() => {
if (!Array.isArray(musics)) return [];
if (!Array.isArray(musics)) return []
return musics
.map((project) => {
const projectId =
typeof project?.id === "string" ? project.id : project?.projectId ?? null;
typeof project?.id === 'string' ? project.id : (project?.projectId ?? null)
const songUrl =
typeof project?.songUrl === "string" && project.songUrl.length > 0
typeof project?.songUrl === 'string' && project.songUrl.length > 0
? project.songUrl
: null;
if (!songUrl) return null;
const descriptorId = projectId ? `project-${projectId}` : songUrl;
: null
if (!songUrl) return null
const descriptorId = projectId ? `project-${projectId}` : songUrl
return {
id: descriptorId,
uri: songUrl,
songUrl,
title: project?.title || "Sans titre",
artist: project?.userName || "",
title: project?.title || 'Sans titre',
artist: project?.userName || '',
artwork: project?.coverUrl ?? null,
coverUrl: project?.coverUrl ?? null,
metadata: {
@@ -76,73 +74,67 @@ const AllMyPlaylist = ({ route }) => {
playlistId: selectedPlaylistId ?? null,
playlistName: playlist?.name ?? null,
},
};
}
})
.filter(Boolean);
}, [musics, playlist?.name, selectedPlaylistId]);
.filter(Boolean)
}, [musics, playlist?.name, selectedPlaylistId])
const handlePlayFromPlaylist = useCallback(
(project) => {
if (!project?.id) return;
if (!project?.id) return
if (Array.isArray(playlistQueueItems) && playlistQueueItems.length > 0) {
const queueIndex = playlistQueueItems.findIndex(
(item) => item?.metadata?.projectId === project.id
);
if (queueIndex >= 0 && typeof setQueue === "function") {
)
if (queueIndex >= 0 && typeof setQueue === 'function') {
setQueue({
items: playlistQueueItems,
id: selectedPlaylistId,
type: "playlist",
type: 'playlist',
name: playlist?.name ?? null,
index: queueIndex,
});
})
}
}
navigateToMusicDetails({
projectId: project.id,
songUrl: project?.songUrl,
project,
});
})
},
[
navigateToMusicDetails,
playlist?.name,
playlistQueueItems,
selectedPlaylistId,
setQueue,
]
);
[navigateToMusicDetails, playlist?.name, playlistQueueItems, selectedPlaylistId, setQueue]
)
// delete playlist
const confirmDelete = () => {
SheetManager.show("Delete", {
SheetManager.show('Delete', {
payload: {
title: "Supprimer la playlist",
message: "Es-tu sûr de vouloir supprimer cette playlist ?",
title: 'Supprimer la playlist',
message: 'Es-tu sûr de vouloir supprimer cette playlist ?',
onConfirm: async () => {
try {
if (selectedPlaylistId) {
await playlistsRef.doc(selectedPlaylistId).delete();
setTooltip({ type: "success", text: "Playlist supprimée" });
await playlistsRef.doc(selectedPlaylistId).delete()
setTooltip({ type: 'success', text: 'Playlist supprimée' })
}
} catch (e) {
console.log("Delete playlist error", e?.message);
console.log('Delete playlist error', e?.message)
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
}
},
},
});
};
})
}
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.libraryBG}
title="Mes Playlists"
width={"80%"}
width={'80%'}
contentContainerStyle={{
paddingBottom: gutters * 2,
}}
@@ -156,7 +148,7 @@ const AllMyPlaylist = ({ route }) => {
</>
)}
>
<View style={{ flexDirection: "row", gap: 20 }}>
<View style={{ flexDirection: 'row', gap: 20 }}>
<View
style={{
flex: 1,
@@ -168,67 +160,62 @@ const AllMyPlaylist = ({ route }) => {
>
<View style={{ flex: 1, gap: 12 }}>
<View style={{ gap: 8 }}>
{(Array.isArray(userPlaylists) ? userPlaylists : []).map(
(item) => {
const isSelected = selectedPlaylistId === item?.id;
{(Array.isArray(userPlaylists) ? userPlaylists : []).map((item) => {
const isSelected = selectedPlaylistId === item?.id
return (
<Pressable
key={item?.id}
onPress={() => setSelectedPlaylistId(item?.id)}
return (
<Pressable key={item?.id} onPress={() => setSelectedPlaylistId(item?.id)}>
<View
style={{
borderRadius: 14,
overflow: 'hidden',
borderWidth: 1,
borderColor: Palette.transparentWhite,
transform: [{ scaleX: isSelected ? 1.04 : 1 }],
}}
>
<View
<BlurView
intensity={30}
style={{
borderRadius: 14,
overflow: "hidden",
borderWidth: 1,
borderColor: Palette.transparentWhite,
transform: [{ scaleX: isSelected ? 1.04 : 1 }],
...Style.containerSpaceBetween,
minHeight: 59,
paddingHorizontal: 16,
paddingVertical: 12,
}}
>
<BlurView
intensity={30}
<Text
style={{
...Style.containerSpaceBetween,
minHeight: 59,
paddingHorizontal: 16,
paddingVertical: 12,
flex: 1,
fontSize: isSelected ? 18 : 16,
color: Palette.white,
fontFamily: isSelected
? FONT_FAMILY.InterSemiBold
: FONT_FAMILY.InterRegular,
marginRight: 12,
flexShrink: 1,
}}
>
<Text
{item?.name || 'Sans nom'}
</Text>
<Pressable>
<Image
source={icons.chevronDown}
style={{
flex: 1,
fontSize: isSelected ? 18 : 16,
color: Palette.white,
fontFamily: isSelected
? FONT_FAMILY.InterSemiBold
: FONT_FAMILY.InterRegular,
marginRight: 12,
flexShrink: 1,
...size({ size: 15 }),
transform: [{ rotate: '-90deg' }],
}}
>
{item?.name || "Sans nom"}
</Text>
<Pressable>
<Image
source={icons.chevronDown}
style={{
...size({ size: 15 }),
transform: [{ rotate: "-90deg" }],
}}
resizeMode="contain"
/>
</Pressable>
</BlurView>
</View>
</Pressable>
);
}
)}
resizeMode="contain"
/>
</Pressable>
</BlurView>
</View>
</Pressable>
)
})}
</View>
<Pressable
onPress={() =>
SheetManager.show("Playlist", {
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
@@ -236,7 +223,7 @@ const AllMyPlaylist = ({ route }) => {
<View
style={{
borderRadius: 12,
overflow: "hidden",
overflow: 'hidden',
borderWidth: 1,
borderColor: Palette.transparentWhite,
}}
@@ -259,11 +246,7 @@ const AllMyPlaylist = ({ route }) => {
>
Ajouter une playlist
</Text>
<Image
source={icons.add}
style={size({ size: 20 })}
resizeMode="contain"
/>
<Image source={icons.add} style={size({ size: 20 })} resizeMode="contain" />
</BlurView>
</View>
</Pressable>
@@ -282,12 +265,9 @@ const AllMyPlaylist = ({ route }) => {
likeTarget={LIKE_TARGET.SONG}
onPress={() => handlePlayFromPlaylist(p)}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuPosition(posTop);
setShowMenu(
(prev) =>
!prev || posTop?.top !== (menuPosition?.top ?? null)
);
setSelectedProjectId(p.id)
setMenuPosition(posTop)
setShowMenu((prev) => !prev || posTop?.top !== (menuPosition?.top ?? null))
}}
/>
))}
@@ -304,7 +284,7 @@ const AllMyPlaylist = ({ route }) => {
</View>
</View>
</Page>
);
};
)
}
export default AllMyPlaylist;
export default AllMyPlaylist
+44 -54
View File
@@ -1,31 +1,24 @@
import { BlurView } from "expo-blur";
import React from "react";
import {
Image,
Platform,
Pressable,
ScrollView,
Text,
View,
} from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import SearchBar from "../../components/SearchBar";
import MobileCoinBadge from "../../components/MobileCoinBadge";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BackTracks from "./components/BackTracks";
import LikedClips from "./components/LikedClips";
import LikedMusic from "./components/LikedMusic";
import LikedPlayback from "./components/LikedPlayback";
import MyClips from "./components/MyClips";
import MyMusic from "./components/MyMusic";
import MyPlaylist from "./components/MyPlaylist";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Platform, Pressable, ScrollView, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background, icons } from '../../assets'
import SearchBar from '../../components/SearchBar'
import MobileCoinBadge from '../../components/MobileCoinBadge'
import ShareBtn from '../../components/ShareBtn/ShareBtn'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import BackTracks from './components/BackTracks'
import LikedClips from './components/LikedClips'
import LikedMusic from './components/LikedMusic'
import LikedPlayback from './components/LikedPlayback'
import MyClips from './components/MyClips'
import MyMusic from './components/MyMusic'
import MyPlaylist from './components/MyPlaylist'
const Library = () => {
const {
@@ -34,19 +27,16 @@ const Library = () => {
userLikedProjects = [],
userLikedPlaybacks = [],
userPlaylists = [],
} = useUser() || {};
} = useUser() || {}
const hasMyMusic = Array.isArray(userProjects) && userProjects.length > 0;
const hasBackTracks =
Array.isArray(userPlaybacks) && userPlaybacks.length > 0;
const hasLikedMusic =
Array.isArray(userLikedProjects) && userLikedProjects.length > 0;
const hasLikedPlayback =
Array.isArray(userLikedPlaybacks) && userLikedPlaybacks.length > 0;
const hasPlaylists = Array.isArray(userPlaylists) && userPlaylists.length > 0;
const hasMyMusic = Array.isArray(userProjects) && userProjects.length > 0
const hasBackTracks = Array.isArray(userPlaybacks) && userPlaybacks.length > 0
const hasLikedMusic = Array.isArray(userLikedProjects) && userLikedProjects.length > 0
const hasLikedPlayback = Array.isArray(userLikedPlaybacks) && userLikedPlaybacks.length > 0
const hasPlaylists = Array.isArray(userPlaylists) && userPlaylists.length > 0
const hasMyClips = false; // À modifier quand les vraies données seront disponibles
const hasLikedClips = false; // À modifier quand les vraies données seront disponibles
const hasMyClips = false // À modifier quand les vraies données seront disponibles
const hasLikedClips = false // À modifier quand les vraies données seront disponibles
const hasAnySection =
hasMyMusic ||
@@ -55,17 +45,17 @@ const Library = () => {
hasLikedPlayback ||
hasPlaylists ||
hasMyClips ||
hasLikedClips;
hasLikedClips
return (
<Page backgroundImg={background.libraryBG} headerType="NONE">
{Platform.OS !== "web" ? (
{Platform.OS !== 'web' ? (
<View
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
marginBottom: 6,
}}
@@ -77,7 +67,7 @@ const Library = () => {
<View style={{ gap: 17, paddingBottom: 20 }}>
<Image
source={icons.musicLandLogo}
style={{ alignSelf: "center", height: 150, resizeMode: "contain" }}
style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }}
/>
<Pressable onPress={() => navigate(Routes.Research)}>
<SearchBar
@@ -94,14 +84,14 @@ const Library = () => {
flexGrow: 1,
gap: 20,
paddingBottom: responsiveHeight(20),
paddingTop: Platform.OS !== "android" ? 20 : 0,
paddingTop: Platform.OS !== 'android' ? 20 : 0,
}}
showsVerticalScrollIndicator={false}
>
<BlurView
intensity={20}
experimentalBlurMethod="dimezisBlurView"
style={{ borderRadius: 12, padding: 12, overflow: "hidden" }}
style={{ borderRadius: 12, padding: 12, overflow: 'hidden' }}
>
<MyMusic />
</BlurView>
@@ -116,8 +106,8 @@ const Library = () => {
<View
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
paddingTop: 40,
}}
>
@@ -127,7 +117,7 @@ const Library = () => {
color: Palette.white,
opacity: 0.8,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Rien n'est disponible encore ici
@@ -137,7 +127,7 @@ const Library = () => {
</ScrollView>
</View>
</Page>
);
};
)
}
export default Library;
export default Library
+83 -107
View File
@@ -1,27 +1,21 @@
import { BlurView } from "expo-blur";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Image, Platform, ScrollView, StyleSheet, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import SearchBar from "../../components/SearchBar";
import useSearch from "../../hooks/useSearch";
import Page from "../../layouts/Page";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BackTracks from "./components/BackTracks";
import LikedMusic from "./components/LikedMusic";
import LikedPlayback from "./components/LikedPlayback";
import MyMusic from "./components/MyMusic";
import MyPlaylist from "./components/MyPlaylist";
import ResearchHeader from "./components/ResearchHeader";
import SearchResultsList from "./components/SearchResultsList";
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background, icons } from '../../assets'
import SearchBar from '../../components/SearchBar'
import useSearch from '../../hooks/useSearch'
import Page from '../../layouts/Page'
import { useUser } from '../../providers/UserDataProvider'
import { Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import BackTracks from './components/BackTracks'
import LikedMusic from './components/LikedMusic'
import LikedPlayback from './components/LikedPlayback'
import MyMusic from './components/MyMusic'
import MyPlaylist from './components/MyPlaylist'
import ResearchHeader from './components/ResearchHeader'
import SearchResultsList from './components/SearchResultsList'
const Library = () => {
const {
userProjects = [],
@@ -29,19 +23,16 @@ const Library = () => {
userLikedProjects = [],
userLikedPlaybacks = [],
userPlaylists = [],
} = useUser() || {};
} = useUser() || {}
const hasMyMusic = Array.isArray(userProjects) && userProjects.length > 0;
const hasBackTracks =
Array.isArray(userPlaybacks) && userPlaybacks.length > 0;
const hasLikedMusic =
Array.isArray(userLikedProjects) && userLikedProjects.length > 0;
const hasLikedPlayback =
Array.isArray(userLikedPlaybacks) && userLikedPlaybacks.length > 0;
const hasPlaylists = Array.isArray(userPlaylists) && userPlaylists.length > 0;
const hasMyMusic = Array.isArray(userProjects) && userProjects.length > 0
const hasBackTracks = Array.isArray(userPlaybacks) && userPlaybacks.length > 0
const hasLikedMusic = Array.isArray(userLikedProjects) && userLikedProjects.length > 0
const hasLikedPlayback = Array.isArray(userLikedPlaybacks) && userLikedPlaybacks.length > 0
const hasPlaylists = Array.isArray(userPlaylists) && userPlaylists.length > 0
const hasMyClips = false; // À modifier quand les vraies données seront disponibles
const hasLikedClips = false; // À modifier quand les vraies données seront disponibles
const hasMyClips = false // À modifier quand les vraies données seront disponibles
const hasLikedClips = false // À modifier quand les vraies données seront disponibles
const hasAnySection =
hasMyMusic ||
@@ -50,7 +41,7 @@ const Library = () => {
hasLikedPlayback ||
hasPlaylists ||
hasMyClips ||
hasLikedClips;
hasLikedClips
const {
search,
@@ -61,96 +52,81 @@ const Library = () => {
musics = [],
playbacks = [],
loading,
} = useSearch();
} = useSearch()
const toggleSelected = useCallback(
(value) => {
setSelected((previous) => (previous === value ? null : value));
setSelected((previous) => (previous === value ? null : value))
},
[setSelected]
);
)
const musicResults = useMemo(
() => (Array.isArray(musics) ? musics : []),
[musics]
);
const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics])
const playbackResults = useMemo(
() => (Array.isArray(playbacks) ? playbacks : []),
[playbacks]
);
const playbackResults = useMemo(() => (Array.isArray(playbacks) ? playbacks : []), [playbacks])
const userResults = useMemo(
() => (Array.isArray(users) ? users : []),
[users]
);
const userResults = useMemo(() => (Array.isArray(users) ? users : []), [users])
const musicsLoading = loading;
const playbacksLoading = loading;
const usersLoading = loading;
const musicsLoading = loading
const playbacksLoading = loading
const usersLoading = loading
const [dropdownVisible, setDropdownVisible] = useState(false);
const searchWrapperRef = useRef(null);
const hasSearchQuery = search.trim().length > 0;
const [dropdownVisible, setDropdownVisible] = useState(false)
const searchWrapperRef = useRef(null)
const hasSearchQuery = search.trim().length > 0
const closeDropdown = useCallback(() => {
setDropdownVisible(false);
setSelected(null);
setSearch("");
}, [setSelected, setSearch]);
setDropdownVisible(false)
setSelected(null)
setSearch('')
}, [setSelected, setSearch])
const handleFocus = () => {
setDropdownVisible(true);
};
setDropdownVisible(true)
}
const shouldShowResults = dropdownVisible;
const shouldBlurContent = dropdownVisible;
const shouldShowResults = dropdownVisible
const shouldBlurContent = dropdownVisible
const handleChangeText = (value) => {
setSearch(value);
setDropdownVisible(true);
};
setSearch(value)
setDropdownVisible(true)
}
useEffect(() => {
if (!dropdownVisible) return undefined;
if (!dropdownVisible) return undefined
const handleClickOutside = (event) => {
if (searchWrapperRef.current?.contains(event.target)) return;
closeDropdown();
};
if (searchWrapperRef.current?.contains(event.target)) return
closeDropdown()
}
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [dropdownVisible, closeDropdown]);
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
}, [dropdownVisible, closeDropdown])
return (
<Page
backgroundImg={background.libraryBgWeb}
headerType="NONE"
width={"80%"}
>
<View
style={{ gap: 17, paddingBottom: 20, zIndex: dropdownVisible ? 40 : 1 }}
>
<Page backgroundImg={background.libraryBgWeb} headerType="NONE" width={'80%'}>
<View style={{ gap: 17, paddingBottom: 20, zIndex: dropdownVisible ? 40 : 1 }}>
<Image
source={icons.musicLandLogo}
style={{ alignSelf: "center", height: 150, resizeMode: "contain" }}
style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }}
/>
<View
ref={searchWrapperRef}
style={{
position: "relative",
alignSelf: "center",
width: "65%",
position: 'relative',
alignSelf: 'center',
width: '65%',
maxWidth: 520,
minWidth: 360,
zIndex: 40,
overflow: "visible",
overflow: 'visible',
}}
>
<SearchBar
@@ -164,12 +140,12 @@ const Library = () => {
{shouldShowResults && (
<View
style={{
position: "absolute",
position: 'absolute',
top: 60,
left: 0,
right: 0,
zIndex: 50,
width: "100%",
width: '100%',
}}
>
<View
@@ -179,7 +155,7 @@ const Library = () => {
padding: 16,
backgroundColor: Palette.ultraLightWhite,
...Style.defaultBorder,
width: "100%",
width: '100%',
zIndex: 50,
elevation: 12,
}}
@@ -205,7 +181,7 @@ const Library = () => {
)}
</View>
</View>
<View style={{ flex: 1, position: "relative" }}>
<View style={{ flex: 1, position: 'relative' }}>
{shouldBlurContent && (
<BlurView
intensity={35}
@@ -215,8 +191,8 @@ const Library = () => {
{
zIndex: 10,
borderRadius: 18,
backgroundColor: "rgba(0, 0, 0, 0.25)",
overflow: "hidden",
backgroundColor: 'rgba(0, 0, 0, 0.25)',
overflow: 'hidden',
},
]}
/>
@@ -226,15 +202,15 @@ const Library = () => {
flexGrow: 1,
gap: 20,
paddingBottom: responsiveHeight(20),
paddingTop: Platform.OS !== "android" ? 20 : 0,
paddingTop: Platform.OS !== 'android' ? 20 : 0,
}}
showsVerticalScrollIndicator={false}
style={{ flex: 1, position: "relative", zIndex: 5 }}
style={{ flex: 1, position: 'relative', zIndex: 5 }}
>
<BlurView
intensity={40}
style={{
flexDirection: "row",
flexDirection: 'row',
gap: 20,
padding: 10,
borderRadius: 12,
@@ -252,7 +228,7 @@ const Library = () => {
<BlurView
style={{
flexDirection: "row",
flexDirection: 'row',
gap: 20,
padding: 10,
borderRadius: 12,
@@ -275,8 +251,8 @@ const Library = () => {
<View
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
paddingTop: 40,
}}
>
@@ -286,7 +262,7 @@ const Library = () => {
color: Palette.white,
opacity: 0.8,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Rien n'est disponible encore ici
@@ -296,7 +272,7 @@ const Library = () => {
</ScrollView>
</View>
</Page>
);
};
)
}
export default Library;
export default Library
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+65 -73
View File
@@ -1,25 +1,25 @@
import { useRoute } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from "react";
import { Image, Pressable, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useDataFromRef } from "react-native-minuit/src/hooks";
import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId";
import { useGlobal } from "reactn";
import { background, icons } from "../../assets";
import MoreMenu from "../../components/MoreMenu";
import { playlistsRef, projectsRef } from "../../config/firebase";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import usePlayer from "../../hooks/usePlayer";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import MusicCard from "./components/MusicCard";
import { useRoute } from '@react-navigation/native'
import React, { useCallback, useMemo, useState } from 'react'
import { Image, Pressable, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { useDataFromRef } from 'react-native-minuit/src/hooks'
import useDataFromArrayId from 'react-native-minuit/src/hooks/useDataFromArrayId'
import { useGlobal } from 'reactn'
import { background, icons } from '../../assets'
import MoreMenu from '../../components/MoreMenu'
import { playlistsRef, projectsRef } from '../../config/firebase'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import usePlayer from '../../hooks/usePlayer'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { gutters } from '../../styles'
import { getProjectLikes, LIKE_TARGET } from '../../utils/likes'
import MusicCard from './components/MusicCard'
const PlaylistDetails = () => {
const [, setTooltip] = useGlobal("_tooltip");
const route = useRoute();
const playlistId = route?.params?.playlistId;
const [, setTooltip] = useGlobal('_tooltip')
const route = useRoute()
const playlistId = route?.params?.playlistId
// Fetch the playlist document
const { data: playlist } = useDataFromRef({
@@ -28,7 +28,7 @@ const PlaylistDetails = () => {
listener: true,
condition: !!playlistId,
refreshArray: [playlistId],
});
})
// Fetch projects by ids from playlist.musics
const { data: musics } = useDataFromArrayId({
@@ -36,32 +36,32 @@ const PlaylistDetails = () => {
arrayId: Array.isArray(playlist?.musics) ? playlist.musics : [],
condition: Array.isArray(playlist?.musics) && playlist.musics.length > 0,
refreshArray: [playlistId, playlist?.musics?.length || 0],
});
})
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const navigateToMusicDetails = useNavigateToMusicDetails();
const { setQueue } = usePlayer() || {};
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const navigateToMusicDetails = useNavigateToMusicDetails()
const { setQueue } = usePlayer() || {}
const playlistQueueItems = useMemo(() => {
if (!Array.isArray(musics)) return [];
if (!Array.isArray(musics)) return []
return musics
.map((project) => {
const projectId =
typeof project?.id === "string" ? project.id : project?.projectId ?? null;
typeof project?.id === 'string' ? project.id : (project?.projectId ?? null)
const songUrl =
typeof project?.songUrl === "string" && project.songUrl.length > 0
typeof project?.songUrl === 'string' && project.songUrl.length > 0
? project.songUrl
: null;
if (!songUrl) return null;
const descriptorId = projectId ? `project-${projectId}` : songUrl;
: null
if (!songUrl) return null
const descriptorId = projectId ? `project-${projectId}` : songUrl
return {
id: descriptorId,
uri: songUrl,
songUrl,
title: project?.title || "Sans titre",
artist: project?.userName || "",
title: project?.title || 'Sans titre',
artist: project?.userName || '',
artwork: project?.coverUrl ?? null,
coverUrl: project?.coverUrl ?? null,
metadata: {
@@ -74,73 +74,67 @@ const PlaylistDetails = () => {
playlistId: playlistId ?? null,
playlistName: playlist?.name ?? null,
},
};
}
})
.filter(Boolean);
}, [musics, playlist?.name, playlistId]);
.filter(Boolean)
}, [musics, playlist?.name, playlistId])
const handlePlayFromPlaylist = useCallback(
(project) => {
if (!project?.id) return;
if (!project?.id) return
if (Array.isArray(playlistQueueItems) && playlistQueueItems.length > 0) {
const queueIndex = playlistQueueItems.findIndex(
(item) => item?.metadata?.projectId === project.id
);
if (queueIndex >= 0 && typeof setQueue === "function") {
)
if (queueIndex >= 0 && typeof setQueue === 'function') {
setQueue({
items: playlistQueueItems,
id: playlistId,
type: "playlist",
type: 'playlist',
name: playlist?.name ?? null,
index: queueIndex,
});
})
}
}
navigateToMusicDetails({
projectId: project.id,
songUrl: project?.songUrl,
project,
});
})
},
[
navigateToMusicDetails,
playlist?.name,
playlistId,
playlistQueueItems,
setQueue,
]
);
[navigateToMusicDetails, playlist?.name, playlistId, playlistQueueItems, setQueue]
)
const confirmDelete = () => {
SheetManager.show("Delete", {
SheetManager.show('Delete', {
payload: {
title: "Supprimer la playlist",
message: "Es-tu sûr de vouloir supprimer cette playlist ?",
title: 'Supprimer la playlist',
message: 'Es-tu sûr de vouloir supprimer cette playlist ?',
onConfirm: async () => {
try {
if (playlistId) {
await playlistsRef.doc(playlistId).delete();
setTooltip({ type: "success", text: "Playlist supprimée" });
await playlistsRef.doc(playlistId).delete()
setTooltip({ type: 'success', text: 'Playlist supprimée' })
}
} catch (e) {
console.log("Delete playlist error", e?.message);
console.log('Delete playlist error', e?.message)
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
} finally {
goBack();
goBack()
}
},
},
});
};
})
}
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.libraryBG}
title={playlist?.name || "Playlist"}
title={playlist?.name || 'Playlist'}
rightComponent={() => (
<Pressable onPress={confirmDelete}>
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
@@ -161,11 +155,9 @@ const PlaylistDetails = () => {
likeTarget={LIKE_TARGET.SONG}
onPress={() => handlePlayFromPlaylist(p)}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuPosition(posTop);
setShowMenu(
(prev) => !prev || posTop?.top !== (menuPosition?.top ?? null)
);
setSelectedProjectId(p.id)
setMenuPosition(posTop)
setShowMenu((prev) => !prev || posTop?.top !== (menuPosition?.top ?? null))
}}
/>
))}
@@ -181,7 +173,7 @@ const PlaylistDetails = () => {
/>
</View>
</Page>
);
};
)
}
export default PlaylistDetails;
export default PlaylistDetails
+10 -10
View File
@@ -1,10 +1,10 @@
import { playlistsRef } from "../../../config/firebase";
import { playlistsRef } from '../../../config/firebase'
export const createPlaylist = async (payload = {}) => {
try {
const createdBy = payload?.createdBy;
const name = payload?.name?.trim?.();
const musics = Array.isArray(payload?.musics) ? payload.musics : [];
const createdBy = payload?.createdBy
const name = payload?.name?.trim?.()
const musics = Array.isArray(payload?.musics) ? payload.musics : []
const doc = {
createdBy,
@@ -12,12 +12,12 @@ export const createPlaylist = async (payload = {}) => {
musics,
createdAt: new Date(),
updatedAt: new Date(),
};
}
const docRef = await playlistsRef.add(doc);
return docRef.id;
const docRef = await playlistsRef.add(doc)
return docRef.id
} catch (error) {
console.error("Error creating playlist:", error);
throw error;
console.error('Error creating playlist:', error)
throw error
}
};
}
+21 -30
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useMemo } from "react";
import { View } from "react-native";
import { background } from "../../assets";
import useSearch from "../../hooks/useSearch";
import Page from "../../layouts/Page";
import { gutters } from "../../styles";
import ResearchHeader from "./components/ResearchHeader";
import SearchResultsList from "./components/SearchResultsList";
import React, { useCallback, useMemo } from 'react'
import { View } from 'react-native'
import { background } from '../../assets'
import useSearch from '../../hooks/useSearch'
import Page from '../../layouts/Page'
import { gutters } from '../../styles'
import ResearchHeader from './components/ResearchHeader'
import SearchResultsList from './components/SearchResultsList'
const Research = () => {
const {
@@ -17,33 +17,24 @@ const Research = () => {
musics = [],
playbacks = [],
loading,
} = useSearch();
} = useSearch()
const toggleSelected = useCallback(
(value) => {
setSelected((previous) => (previous === value ? null : value));
setSelected((previous) => (previous === value ? null : value))
},
[setSelected],
);
[setSelected]
)
const musicResults = useMemo(
() => (Array.isArray(musics) ? musics : []),
[musics],
);
const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics])
const playbackResults = useMemo(
() => (Array.isArray(playbacks) ? playbacks : []),
[playbacks],
);
const playbackResults = useMemo(() => (Array.isArray(playbacks) ? playbacks : []), [playbacks])
const userResults = useMemo(
() => (Array.isArray(users) ? users : []),
[users],
);
const userResults = useMemo(() => (Array.isArray(users) ? users : []), [users])
const musicsLoading = loading;
const playbacksLoading = loading;
const usersLoading = loading;
const musicsLoading = loading
const playbacksLoading = loading
const usersLoading = loading
return (
<Page
@@ -72,7 +63,7 @@ const Research = () => {
/>
</View>
</Page>
);
};
)
}
export default Research;
export default Research
+31 -47
View File
@@ -1,47 +1,35 @@
import React, { useCallback, useState } from "react";
import {
FlatList,
Image,
Pressable,
Text,
View,
useWindowDimensions,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import GradientButton from "../../../components/GradientButton";
import useLayoutType from "../../../hooks/useLayoutType";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { useUserData } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import CardContainer from "./CardContainer";
import React, { useCallback, useState } from 'react'
import { FlatList, Image, Pressable, Text, View, useWindowDimensions } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import GradientButton from '../../../components/GradientButton'
import useLayoutType from '../../../hooks/useLayoutType'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { useUserData } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import CardContainer from './CardContainer'
const BackTracks = () => {
const { userPlaybacks } = useUserData();
const { isWeb } = useLayoutType();
const items = Array.isArray(userPlaybacks)
? userPlaybacks.slice(0, isWeb ? 2 : 3)
: [];
const { width: screenWidth } = useWindowDimensions();
const [containerWidth, setContainerWidth] = useState(screenWidth);
const numColumns = isWeb ? 2 : 3;
const gap = 8;
const itemWidth = Math.max(
0,
Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns)
);
const { userPlaybacks } = useUserData()
const { isWeb } = useLayoutType()
const items = Array.isArray(userPlaybacks) ? userPlaybacks.slice(0, isWeb ? 2 : 3) : []
const { width: screenWidth } = useWindowDimensions()
const [containerWidth, setContainerWidth] = useState(screenWidth)
const numColumns = isWeb ? 2 : 3
const gap = 8
const itemWidth = Math.max(0, Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns))
const openPlaybackPicker = useCallback(() => {
SheetManager.show("PlaybackPicker");
}, []);
SheetManager.show('PlaybackPicker')
}, [])
return (
<CardContainer
label="Mes Playback"
onPress={() =>
navigate(Routes.AllMyList, {
title: "Mes playback",
scope: "playback",
title: 'Mes playback',
scope: 'playback',
liked: false,
})
}
@@ -58,15 +46,11 @@ const BackTracks = () => {
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ width: itemWidth }} key={item.id}>
<Pressable
onPress={() =>
navigate(Routes.Playbacks, { projectId: item.id })
}
>
<Pressable onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}>
<Image
source={{ uri: item?.thumbnailUrl || item?.coverUrl || "" }}
source={{ uri: item?.thumbnailUrl || item?.coverUrl || '' }}
style={{
width: "100%",
width: '100%',
height: Math.round((itemWidth * 16) / 9),
borderRadius: 10,
}}
@@ -76,10 +60,10 @@ const BackTracks = () => {
)}
/>
) : (
<View style={{ flexDirection: "column", gap: 10 }}>
<View style={{ flexDirection: 'column', gap: 10 }}>
<Text
style={{
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
fontSize: 16,
@@ -96,7 +80,7 @@ const BackTracks = () => {
)}
</View>
</CardContainer>
);
};
)
}
export default BackTracks;
export default BackTracks
+11 -11
View File
@@ -1,15 +1,15 @@
import React from "react";
import { Image, Pressable, StyleSheet, Text, View } from "react-native";
import { icons } from "../../../assets";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
const CardContainer = ({ children, label = "", onPress, onPressPlus }) => {
import React from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
import { icons } from '../../../assets'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
const CardContainer = ({ children, label = '', onPress, onPressPlus }) => {
return (
<View style={{ gap: 12 }}>
<View style={{ ...Style.containerSpaceBetween }}>
<Text style={styles.label}>{label}</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: 20 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 20 }}>
<Pressable onPress={onPressPlus}>
<Image source={icons.add} style={{ width: 15, height: 15 }} />
</Pressable>
@@ -20,10 +20,10 @@ const CardContainer = ({ children, label = "", onPress, onPressPlus }) => {
</View>
{children}
</View>
);
};
)
}
export default CardContainer;
export default CardContainer
const styles = StyleSheet.create({
label: {
@@ -36,4 +36,4 @@ const styles = StyleSheet.create({
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
+9 -9
View File
@@ -1,8 +1,8 @@
import { View, Text, Image } from "react-native";
import React from "react";
import CardContainer from "./CardContainer";
import { Style } from "../../../styles";
import { img } from "../../../assets";
import { View, Text, Image } from 'react-native'
import React from 'react'
import CardContainer from './CardContainer'
import { Style } from '../../../styles'
import { img } from '../../../assets'
const LikedClips = () => {
return (
@@ -12,13 +12,13 @@ const LikedClips = () => {
<View style={{ flex: 1 }} key={index}>
<Image
source={img.placeholder3}
style={{ width: "100%", height: 172, borderRadius: 10 }}
style={{ width: '100%', height: 172, borderRadius: 10 }}
/>
</View>
))}
</View>
</CardContainer>
);
};
)
}
export default LikedClips;
export default LikedClips
+40 -53
View File
@@ -1,5 +1,5 @@
import { Image as ExpoImage } from "expo-image";
import React, { useCallback, useState } from "react";
import { Image as ExpoImage } from 'expo-image'
import React, { useCallback, useState } from 'react'
import {
FlatList,
Pressable,
@@ -7,62 +7,49 @@ import {
Text,
useWindowDimensions,
View,
} from "react-native";
import { img } from "../../../assets";
import GradientButton from "../../../components/GradientButton";
import useLayoutType from "../../../hooks/useLayoutType";
import useNavigateToMusicDetails from "../../../hooks/useNavigateToMusicDetails";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { useUser } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import CardContainer from "./CardContainer";
} from 'react-native'
import { img } from '../../../assets'
import GradientButton from '../../../components/GradientButton'
import useLayoutType from '../../../hooks/useLayoutType'
import useNavigateToMusicDetails from '../../../hooks/useNavigateToMusicDetails'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { useUser } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import CardContainer from './CardContainer'
const LikedMusic = () => {
const { userLikedProjects = [] } = useUser();
const items = Array.isArray(userLikedProjects)
? userLikedProjects.slice(0, 3)
: [];
const { width: screenWidth } = useWindowDimensions();
const { isWeb } = useLayoutType();
const [containerWidth, setContainerWidth] = useState(screenWidth);
const numColumns = isWeb ? 2 : 3;
const gap = 8;
const itemWidth = Math.max(
0,
Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns)
);
const navigateToMusicDetails = useNavigateToMusicDetails();
const { userLikedProjects = [] } = useUser()
const items = Array.isArray(userLikedProjects) ? userLikedProjects.slice(0, 3) : []
const { width: screenWidth } = useWindowDimensions()
const { isWeb } = useLayoutType()
const [containerWidth, setContainerWidth] = useState(screenWidth)
const numColumns = isWeb ? 2 : 3
const gap = 8
const itemWidth = Math.max(0, Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns))
const navigateToMusicDetails = useNavigateToMusicDetails()
const resolveCoverUri = useCallback((project) => {
if (!project) return null;
const isValid = (value) =>
typeof value === "string" && value.trim().length > 0;
if (!project) return null
const isValid = (value) => typeof value === 'string' && value.trim().length > 0
if (isValid(project?.coverUrl)) {
return project.coverUrl;
return project.coverUrl
}
const cover = project?.cover || {};
const coverCandidates = [
cover?.result,
cover?.finalUrl,
cover?.generatedBackground,
];
const cover = project?.cover || {}
const coverCandidates = [cover?.result, cover?.finalUrl, cover?.generatedBackground]
if (Array.isArray(cover?.options)) {
coverCandidates.push(
...cover.options.flatMap((option) => [
option?.finalUrl,
option?.generatedUrl,
])
);
...cover.options.flatMap((option) => [option?.finalUrl, option?.generatedUrl])
)
}
return coverCandidates.find(isValid) || null;
}, []);
return coverCandidates.find(isValid) || null
}, [])
return (
<CardContainer
label="Musiques likées"
onPress={() =>
navigate(Routes.AllMyList, {
title: "Musiques likées",
scope: "music",
title: 'Musiques likées',
scope: 'music',
liked: true,
})
}
@@ -97,7 +84,7 @@ const LikedMusic = () => {
contentFit="cover"
transition={100}
style={{
width: "100%",
width: '100%',
height: itemWidth,
borderRadius: 10,
}}
@@ -106,7 +93,7 @@ const LikedMusic = () => {
<RNImage
source={img.placeholder3}
style={{
width: "100%",
width: '100%',
height: itemWidth,
borderRadius: 10,
}}
@@ -116,10 +103,10 @@ const LikedMusic = () => {
)}
/>
) : (
<View style={{ flexDirection: "column", gap: 10 }}>
<View style={{ flexDirection: 'column', gap: 10 }}>
<Text
style={{
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
fontSize: 16,
@@ -135,7 +122,7 @@ const LikedMusic = () => {
</View>
)}
</CardContainer>
);
};
)
}
export default LikedMusic;
export default LikedMusic
+28 -44
View File
@@ -1,41 +1,29 @@
import React, { useState } from "react";
import {
FlatList,
Image,
Pressable,
Text,
View,
useWindowDimensions,
} from "react-native";
import GradientButton from "../../../components/GradientButton";
import useLayoutType from "../../../hooks/useLayoutType";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { useUserData } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import CardContainer from "./CardContainer";
import React, { useState } from 'react'
import { FlatList, Image, Pressable, Text, View, useWindowDimensions } from 'react-native'
import GradientButton from '../../../components/GradientButton'
import useLayoutType from '../../../hooks/useLayoutType'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { useUserData } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import CardContainer from './CardContainer'
const LikedPlayback = () => {
const { isWeb } = useLayoutType();
const { userLikedPlaybacks } = useUserData();
const items = Array.isArray(userLikedPlaybacks)
? userLikedPlaybacks.slice(0, isWeb ? 2 : 3)
: [];
const { width: screenWidth } = useWindowDimensions();
const [containerWidth, setContainerWidth] = useState(screenWidth);
const numColumns = isWeb ? 2 : 3;
const gap = 8;
const itemWidth = Math.max(
0,
Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns)
);
const { isWeb } = useLayoutType()
const { userLikedPlaybacks } = useUserData()
const items = Array.isArray(userLikedPlaybacks) ? userLikedPlaybacks.slice(0, isWeb ? 2 : 3) : []
const { width: screenWidth } = useWindowDimensions()
const [containerWidth, setContainerWidth] = useState(screenWidth)
const numColumns = isWeb ? 2 : 3
const gap = 8
const itemWidth = Math.max(0, Math.floor((containerWidth - gap * (numColumns - 1)) / numColumns))
return (
<CardContainer
label="Playback likés"
onPress={() =>
navigate(Routes.AllMyList, {
title: "Playback likés",
scope: "playback",
title: 'Playback likés',
scope: 'playback',
liked: true,
})
}
@@ -52,15 +40,11 @@ const LikedPlayback = () => {
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ width: itemWidth }} key={item.id}>
<Pressable
onPress={() =>
navigate(Routes.Playbacks, { projectId: item.id })
}
>
<Pressable onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}>
<Image
source={{ uri: item?.thumbnailUrl || item?.coverUrl || "" }}
source={{ uri: item?.thumbnailUrl || item?.coverUrl || '' }}
style={{
width: "100%",
width: '100%',
height: Math.round((itemWidth * 16) / 9),
borderRadius: 10,
}}
@@ -70,10 +54,10 @@ const LikedPlayback = () => {
)}
/>
) : (
<View style={{ flexDirection: "column", gap: 10 }}>
<View style={{ flexDirection: 'column', gap: 10 }}>
<Text
style={{
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
fontSize: 16,
@@ -90,7 +74,7 @@ const LikedPlayback = () => {
)}
</View>
</CardContainer>
);
};
)
}
export default LikedPlayback;
export default LikedPlayback
+58 -68
View File
@@ -1,95 +1,88 @@
import { BlurView } from "expo-blur";
import { Image as ExpoImage } from "expo-image";
import React, { useEffect, useRef, useState } from "react";
import {
Dimensions,
Pressable,
Image as RNImage,
StyleSheet,
Text,
View,
} from "react-native";
import { responsiveWidth } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { icons, img } from "../../../assets";
import PressableScale from "../../../components/PressableScale";
import { LIKE_TARGET, toggleProjectLike } from "../../../utils/likes";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
import { BlurView } from 'expo-blur'
import { Image as ExpoImage } from 'expo-image'
import React, { useEffect, useRef, useState } from 'react'
import { Dimensions, Pressable, Image as RNImage, StyleSheet, Text, View } from 'react-native'
import { responsiveWidth } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { icons, img } from '../../../assets'
import PressableScale from '../../../components/PressableScale'
import { LIKE_TARGET, toggleProjectLike } from '../../../utils/likes'
import { ensureAuthenticated } from '../../../utils/authRedirect'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { size } from '../../../styles/Style'
const MusicCard = ({
onPress,
onPressMore,
title = "Sans titre",
subtitle = "MusicLand",
title = 'Sans titre',
subtitle = 'MusicLand',
imageUri = null,
projectId = null,
likedBy = [],
likeTarget = LIKE_TARGET.SONG,
}) => {
const [currentUID] = useGlobal("currentUID");
const [selected, setSelected] = useState(false);
const [layout, setLayout] = useState(null);
const [currentUID] = useGlobal('currentUID')
const [selected, setSelected] = useState(false)
const [layout, setLayout] = useState(null)
const [menuPos, setMenuPos] = useState({
top: 0,
right: 0,
left: 0,
width: 0,
height: 0,
});
const cardRef = useRef(null);
const moreButtonRef = useRef(null);
})
const cardRef = useRef(null)
const moreButtonRef = useRef(null)
useEffect(() => {
if (layout) {
const top = (layout?.y || 0) + 45;
setMenuPos((prev) => ({ ...prev, top }));
const top = (layout?.y || 0) + 45
setMenuPos((prev) => ({ ...prev, top }))
}
}, [layout]);
}, [layout])
useEffect(() => {
if (Array.isArray(likedBy) && currentUID) {
setSelected(likedBy.includes(currentUID));
setSelected(likedBy.includes(currentUID))
}
}, [JSON.stringify(likedBy), currentUID]);
}, [JSON.stringify(likedBy), currentUID])
const toggleLike = async () => {
if (!projectId) return;
if (!ensureAuthenticated(currentUID)) return;
const next = !selected;
setSelected(next);
if (!projectId) return
if (!ensureAuthenticated(currentUID)) return
const next = !selected
setSelected(next)
try {
await toggleProjectLike({
projectId,
target: likeTarget,
currentUID,
next,
});
})
} catch (e) {
// rollback on failure
setSelected(!next);
setSelected(!next)
}
};
}
const onPressMenu = () => {
const computeAnchor = (x = 0, y = 0, width = 0, height = 0) => {
const windowWidth = Dimensions.get("window").width || 0;
const top = (y || 0) + (height || 0) + 8;
const right = Math.max(0, windowWidth - ((x || 0) + (width || 0)));
const left = Math.max(0, x || 0);
const anchor = { top, right, left, width, height };
setMenuPos(anchor);
onPressMore?.(anchor);
};
const windowWidth = Dimensions.get('window').width || 0
const top = (y || 0) + (height || 0) + 8
const right = Math.max(0, windowWidth - ((x || 0) + (width || 0)))
const left = Math.max(0, x || 0)
const anchor = { top, right, left, width, height }
setMenuPos(anchor)
onPressMore?.(anchor)
}
if (moreButtonRef?.current?.measureInWindow) {
try {
moreButtonRef.current.measureInWindow((x, y, width, height) => {
computeAnchor(x, y, width, height);
});
return;
computeAnchor(x, y, width, height)
})
return
} catch (e) {
// fallback to card measurement below
}
@@ -99,15 +92,15 @@ const MusicCard = ({
if (cardRef?.current?.measureInWindow) {
try {
cardRef.current.measureInWindow((x, y, width, height) => {
computeAnchor(x, y, width, height);
});
return;
computeAnchor(x, y, width, height)
})
return
} catch (e) {
// fallback to relative layout position
}
}
onPressMore?.(menuPos);
};
onPressMore?.(menuPos)
}
return (
<>
@@ -130,10 +123,7 @@ const MusicCard = ({
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
) : (
<RNImage
source={img.placeholder}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
<RNImage source={img.placeholder} style={{ ...size({ size: 60 }), borderRadius: 12 }} />
)}
<View style={styles.blurContainer}>
<BlurView
@@ -149,9 +139,9 @@ const MusicCard = ({
>
<View style={{ gap: 3, flexShrink: 1, paddingVertical: 11 }}>
<Text numberOfLines={2} style={styles.title}>
{title || "Sans titre"}
{title || 'Sans titre'}
</Text>
<Text style={styles.subTitle}>{subtitle || "MusicLand"}</Text>
<Text style={styles.subTitle}>{subtitle || 'MusicLand'}</Text>
</View>
<View
style={{
@@ -187,17 +177,17 @@ const MusicCard = ({
/>
)} */}
</>
);
};
)
}
export default MusicCard;
export default MusicCard
const styles = StyleSheet.create({
blurContainer: {
borderRadius: 12,
overflow: "hidden",
overflow: 'hidden',
flex: 1,
height: "100%",
height: '100%',
},
title: {
fontSize: 16,
@@ -214,4 +204,4 @@ const styles = StyleSheet.create({
filterContainer: {
backgroundColor: Palette.tran,
},
});
})
+9 -9
View File
@@ -1,8 +1,8 @@
import { View, Text, Image } from "react-native";
import React from "react";
import CardContainer from "./CardContainer";
import { Style } from "../../../styles";
import { img } from "../../../assets";
import { View, Text, Image } from 'react-native'
import React from 'react'
import CardContainer from './CardContainer'
import { Style } from '../../../styles'
import { img } from '../../../assets'
const MyClips = () => {
return (
@@ -12,13 +12,13 @@ const MyClips = () => {
<View style={{ flex: 1 }} key={index}>
<Image
source={img.placeholder3}
style={{ width: "100%", height: 172, borderRadius: 10 }}
style={{ width: '100%', height: 172, borderRadius: 10 }}
/>
</View>
))}
</View>
</CardContainer>
);
};
)
}
export default MyClips;
export default MyClips
+46 -50
View File
@@ -1,37 +1,37 @@
import React, { useState } from "react";
import { Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useGlobal } from "reactn";
import GradientButton from "../../../components/GradientButton";
import MoreMenu from "../../../components/MoreMenu";
import { projectsRef } from "../../../config/firebase";
import useNavigateToMusicDetails from "../../../hooks/useNavigateToMusicDetails";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { useUser } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { getProjectLikes, LIKE_TARGET } from "../../../utils/likes";
import CardContainer from "./CardContainer";
import MusicCard from "./MusicCard";
import React, { useState } from 'react'
import { Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { useGlobal } from 'reactn'
import GradientButton from '../../../components/GradientButton'
import MoreMenu from '../../../components/MoreMenu'
import { projectsRef } from '../../../config/firebase'
import useNavigateToMusicDetails from '../../../hooks/useNavigateToMusicDetails'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { useUser } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { getProjectLikes, LIKE_TARGET } from '../../../utils/likes'
import CardContainer from './CardContainer'
import MusicCard from './MusicCard'
const MyMusic = () => {
const { userProjects = [] } = useUser();
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const projects = Array.isArray(userProjects) ? userProjects.slice(0, 3) : [];
const hasProjects = projects.length > 0;
const { userProjects = [] } = useUser()
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const projects = Array.isArray(userProjects) ? userProjects.slice(0, 3) : []
const hasProjects = projects.length > 0
const [, setTooltip] = useGlobal("_tooltip");
const navigateToMusicDetails = useNavigateToMusicDetails();
const [, setTooltip] = useGlobal('_tooltip')
const navigateToMusicDetails = useNavigateToMusicDetails()
return (
<CardContainer
label="Mes musiques"
onPress={() =>
navigate(Routes.AllMyList, {
title: "Mes musiques",
scope: "music",
title: 'Mes musiques',
scope: 'music',
liked: false,
})
}
@@ -57,12 +57,9 @@ const MyMusic = () => {
})
}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuPosition(posTop);
setShowMenu(
(prev) =>
!prev || posTop?.top !== (menuPosition?.top ?? null)
);
setSelectedProjectId(p.id)
setMenuPosition(posTop)
setShowMenu((prev) => !prev || posTop?.top !== (menuPosition?.top ?? null))
}}
/>
))}
@@ -75,26 +72,25 @@ const MyMusic = () => {
projectId={selectedProjectId}
extraItems={[
{
label: "Supprimer",
label: 'Supprimer',
onPress: () =>
SheetManager.show("Delete", {
SheetManager.show('Delete', {
payload: {
title: "Supprimer le projet",
message:
"Cette action supprimera définitivement ce projet.",
title: 'Supprimer le projet',
message: 'Cette action supprimera définitivement ce projet.',
onConfirm: async () => {
try {
if (!selectedProjectId) return;
await projectsRef.doc(selectedProjectId).delete();
if (!selectedProjectId) return
await projectsRef.doc(selectedProjectId).delete()
setTooltip({
type: "success",
text: "Projet supprimé",
});
type: 'success',
text: 'Projet supprimé',
})
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
}
},
},
@@ -104,10 +100,10 @@ const MyMusic = () => {
/>
</View>
) : (
<View style={{ flexDirection: "column", gap: 10 }}>
<View style={{ flexDirection: 'column', gap: 10 }}>
<Text
style={{
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
fontSize: 16,
@@ -123,7 +119,7 @@ const MyMusic = () => {
</View>
)}
</CardContainer>
);
};
)
}
export default MyMusic;
export default MyMusic
+32 -34
View File
@@ -1,30 +1,28 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { icons } from "../../../assets";
import GradientButton from "../../../components/GradientButton";
import useLayoutType from "../../../hooks/useLayoutType";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { useUser } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import Style, { size } from "../../../styles/Style";
import CardContainer from "./CardContainer";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Platform, Pressable, Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { icons } from '../../../assets'
import GradientButton from '../../../components/GradientButton'
import useLayoutType from '../../../hooks/useLayoutType'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { useUser } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import Style, { size } from '../../../styles/Style'
import CardContainer from './CardContainer'
const MyPlaylist = () => {
const { userPlaylists = [] } = useUser();
const playlists = Array.isArray(userPlaylists)
? userPlaylists.slice(0, 2)
: [];
const { isWeb } = useLayoutType();
const hasPlaylists = playlists.length > 0;
const { userPlaylists = [] } = useUser()
const playlists = Array.isArray(userPlaylists) ? userPlaylists.slice(0, 2) : []
const { isWeb } = useLayoutType()
const hasPlaylists = playlists.length > 0
return (
<CardContainer
label="Mes Playlists"
onPress={() => navigate(Routes.AllMyPlaylist)}
onPressPlus={() =>
SheetManager.show("Playlist", {
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
@@ -34,18 +32,18 @@ const MyPlaylist = () => {
playlists.map((item) => (
<Pressable
key={item.id}
style={{ borderRadius: 12, overflow: "hidden" }}
style={{ borderRadius: 12, overflow: 'hidden' }}
onPress={() => {
console.log("isWeb", isWeb);
console.log('isWeb', isWeb)
if (isWeb) {
navigate(Routes.AllMyPlaylist, { playListId: item.id });
navigate(Routes.AllMyPlaylist, { playListId: item.id })
} else {
navigate(Routes.PlaylistDetails, { playlistId: item.id });
navigate(Routes.PlaylistDetails, { playlistId: item.id })
}
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
...Style.containerSpaceBetween,
minHeight: 59,
@@ -66,13 +64,13 @@ const MyPlaylist = () => {
flexShrink: 1,
}}
>
{item?.name || "Sans nom"}
{item?.name || 'Sans nom'}
</Text>
<Image
source={icons.chevronDown}
style={{
...size({ size: 15 }),
transform: [{ rotate: "-90deg" }],
transform: [{ rotate: '-90deg' }],
}}
resizeMode="contain"
/>
@@ -80,10 +78,10 @@ const MyPlaylist = () => {
</Pressable>
))
) : (
<View style={{ flexDirection: "column", gap: 10 }}>
<View style={{ flexDirection: 'column', gap: 10 }}>
<Text
style={{
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
fontSize: 16,
@@ -94,7 +92,7 @@ const MyPlaylist = () => {
<GradientButton
containerStyle={{ padding: 2 }}
onPress={() =>
SheetManager.show("Playlist", {
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
@@ -104,7 +102,7 @@ const MyPlaylist = () => {
)}
</View>
</CardContainer>
);
};
)
}
export default MyPlaylist;
export default MyPlaylist
@@ -1,15 +1,15 @@
import { View, Text, Pressable, StyleSheet, Platform } from "react-native";
import React from "react";
import SearchBar from "../../../components/SearchBar";
import { Palette, Style } from "../../../styles";
import BorderGradient from "../../../components/BorderGradient/BorderGradient";
import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { View, Text, Pressable, StyleSheet, Platform } from 'react-native'
import React from 'react'
import SearchBar from '../../../components/SearchBar'
import { Palette, Style } from '../../../styles'
import BorderGradient from '../../../components/BorderGradient/BorderGradient'
import { BlurView } from 'expo-blur'
import { FONT_FAMILY } from '../../../styles/Fonts'
const ResearchHeader = ({
onPress,
selected,
searchValue = "",
searchValue = '',
onChangeSearch = () => {},
showSearchBar = true,
autoFocusSearch = true,
@@ -31,14 +31,11 @@ const ResearchHeader = ({
gap: 6,
}}
>
{["Musiques", "Playback", "Profils"].map((item, index) => (
{['Musiques', 'Playback', 'Profils'].map((item, index) => (
<Pressable key={index} onPress={() => onPress(item)}>
<BorderGradient
gradientProps={{
colors:
selected === item
? ["#F94697", "#7023F7"]
: [Palette.tran, Palette.tran],
colors: selected === item ? ['#F94697', '#7023F7'] : [Palette.tran, Palette.tran],
locations: [0, 1],
start: { x: 0, y: 0 },
end: { x: 1, y: 0 },
@@ -46,10 +43,7 @@ const ResearchHeader = ({
style={styles.borderGradient}
/>
<View style={styles.blurContainer}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
style={styles.blurView}
>
<BlurView intensity={Platform.OS !== 'ios' ? 10 : 20} style={styles.blurView}>
<Text style={styles.menu}>{item}</Text>
</BlurView>
</View>
@@ -57,10 +51,10 @@ const ResearchHeader = ({
))}
</View>
</View>
);
};
)
}
export default ResearchHeader;
export default ResearchHeader
const styles = StyleSheet.create({
menu: {
@@ -72,21 +66,21 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderRadius: 100,
height: 30,
position: "absolute",
position: 'absolute',
zIndex: 1,
width: "100%",
width: '100%',
},
blurContainer: {
height: 30,
zIndex: -1,
borderRadius: 100,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
},
blurView: {
width: "100%",
height: "100%",
justifyContent: "center",
width: '100%',
height: '100%',
justifyContent: 'center',
paddingHorizontal: 10,
},
});
})
@@ -1,20 +1,20 @@
import React, { useCallback, useMemo, useState } from "react";
import { Pressable, ScrollView, Text, View } from "react-native";
import MoreMenu from "../../../components/MoreMenu";
import ProfilePicture from "../../../components/ProfilePicture";
import useNavigateToMusicDetails from "../../../hooks/useNavigateToMusicDetails";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { getArtistDisplayName } from "../../../utils/artistName";
import { getProjectLikes, LIKE_TARGET } from "../../../utils/likes";
import CreateLyricsHeader from "../../Writing/components/CreateLyricsHeader";
import MusicCard from "./MusicCard";
import { defaultAvatar } from "../../../data/data";
import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, ScrollView, Text, View } from 'react-native'
import MoreMenu from '../../../components/MoreMenu'
import ProfilePicture from '../../../components/ProfilePicture'
import useNavigateToMusicDetails from '../../../hooks/useNavigateToMusicDetails'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { getArtistDisplayName } from '../../../utils/artistName'
import { getProjectLikes, LIKE_TARGET } from '../../../utils/likes'
import CreateLyricsHeader from '../../Writing/components/CreateLyricsHeader'
import MusicCard from './MusicCard'
import { defaultAvatar } from '../../../data/data'
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<View style={{ flex: 1, alignItems: 'center', paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
@@ -26,7 +26,7 @@ const EmptyText = ({ text }) => (
{text}
</Text>
</View>
);
)
const SearchResultsList = ({
selected,
@@ -40,105 +40,89 @@ const SearchResultsList = ({
contentContainerStyle,
style,
}) => {
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const navigateToMusicDetails = useNavigateToMusicDetails();
const navigateToMusicDetails = useNavigateToMusicDetails()
const resolveCoverUri = useCallback(
(project, { preferThumbnail = false } = {}) => {
const isValid = (value) =>
typeof value === "string" && value.trim().length > 0;
if (!project) {
return null;
const resolveCoverUri = useCallback((project, { preferThumbnail = false } = {}) => {
const isValid = (value) => typeof value === 'string' && value.trim().length > 0
if (!project) {
return null
}
const cover = project?.cover || {}
const thumbnailCandidates = [project?.thumbnailUrl, project?.songThumbnailUrl].filter(isValid)
if (preferThumbnail) {
const preferredThumbnail = thumbnailCandidates.find(isValid)
if (preferredThumbnail) {
return preferredThumbnail
}
}
const cover = project?.cover || {};
const thumbnailCandidates = [
project?.thumbnailUrl,
project?.songThumbnailUrl,
].filter(isValid);
if (isValid(project?.coverUrl)) {
return project.coverUrl
}
if (preferThumbnail) {
const preferredThumbnail = thumbnailCandidates.find(isValid);
if (preferredThumbnail) {
return preferredThumbnail;
}
}
const coverCandidates = [cover?.result, cover?.finalUrl, cover?.generatedBackground]
if (isValid(project?.coverUrl)) {
return project.coverUrl;
}
if (Array.isArray(cover?.options)) {
coverCandidates.push(
...cover.options.flatMap((option) => [option?.finalUrl, option?.generatedUrl])
)
}
const coverCandidates = [
cover?.result,
cover?.finalUrl,
cover?.generatedBackground,
];
if (Array.isArray(cover?.options)) {
coverCandidates.push(
...cover.options.flatMap((option) => [
option?.finalUrl,
option?.generatedUrl,
]),
);
}
return coverCandidates.find(isValid) || null;
},
[],
);
return coverCandidates.find(isValid) || null
}, [])
const handleMusicPress = (project) => {
const didNavigate = navigateToMusicDetails({
projectId: project?.id,
songUrl: project?.songUrl,
project,
});
})
if (didNavigate) {
onResultSelected?.();
onResultSelected?.()
}
};
}
const handlePlaybackPress = (project) => {
if (!project?.id) {
return;
return
}
navigate(Routes.Playbacks, { projectId: project.id });
onResultSelected?.();
};
navigate(Routes.Playbacks, { projectId: project.id })
onResultSelected?.()
}
const handleProfilePress = (userId) => {
navigate(Routes.SingerProfile, { userId });
onResultSelected?.();
};
navigate(Routes.SingerProfile, { userId })
onResultSelected?.()
}
const musicItems = useMemo(() => {
if (!Array.isArray(musics)) {
return [];
return []
}
if (selected === "Musiques") {
return musics;
if (selected === 'Musiques') {
return musics
}
return musics.filter((project) => project?.hasPlayback !== true);
}, [musics, selected]);
return musics.filter((project) => project?.hasPlayback !== true)
}, [musics, selected])
const shouldShowMusics =
(!selected && (musicItems.length > 0 || musicsLoading)) ||
selected === "Musiques";
(!selected && (musicItems.length > 0 || musicsLoading)) || selected === 'Musiques'
const shouldShowPlaybacks =
(!selected && (playbacks.length > 0 || playbacksLoading)) ||
selected === "Playback";
(!selected && (playbacks.length > 0 || playbacksLoading)) || selected === 'Playback'
const shouldShowUsers =
(!selected && (users.length > 0 || usersLoading)) || selected === "Profils";
(!selected && (users.length > 0 || usersLoading)) || selected === 'Profils'
return (
<View style={[{ flex: 1 }, style]}>
@@ -154,7 +138,7 @@ const SearchResultsList = ({
musicItems.slice(0, 6).map((project) => (
<MusicCard
key={project.id}
title={project?.title || "Sans titre"}
title={project?.title || 'Sans titre'}
subtitle={project?.userName}
imageUri={resolveCoverUri(project)}
projectId={project?.id}
@@ -162,13 +146,12 @@ const SearchResultsList = ({
likeTarget={LIKE_TARGET.SONG}
onPress={() => handleMusicPress(project)}
onPressMore={(positionTop) => {
setSelectedProjectId(project.id);
setMenuPosition(positionTop);
setSelectedProjectId(project.id)
setMenuPosition(positionTop)
setShowMenu(
(previous) =>
!previous ||
positionTop?.top !== (menuPosition?.top ?? null),
);
!previous || positionTop?.top !== (menuPosition?.top ?? null)
)
}}
/>
))}
@@ -178,16 +161,16 @@ const SearchResultsList = ({
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
alignSelf: "center",
alignSelf: 'center',
}}
>
{"Chargement…"}
{'Chargement…'}
</Text>
)}
</>
)}
{musicItems.length === 0 && !musicsLoading && (
<EmptyText text={"Aucune musique trouvée"} />
<EmptyText text={'Aucune musique trouvée'} />
)}
</View>
)}
@@ -199,7 +182,7 @@ const SearchResultsList = ({
playbacks.slice(0, 6).map((project) => (
<MusicCard
key={project.id}
title={project?.title || "Sans titre"}
title={project?.title || 'Sans titre'}
subtitle={project?.userName}
imageUri={resolveCoverUri(project, {
preferThumbnail: true,
@@ -209,13 +192,12 @@ const SearchResultsList = ({
likeTarget={LIKE_TARGET.PLAYBACK}
onPress={() => handlePlaybackPress(project)}
onPressMore={(positionTop) => {
setSelectedProjectId(project.id);
setMenuPosition(positionTop);
setSelectedProjectId(project.id)
setMenuPosition(positionTop)
setShowMenu(
(previous) =>
!previous ||
positionTop?.top !== (menuPosition?.top ?? null),
);
!previous || positionTop?.top !== (menuPosition?.top ?? null)
)
}}
/>
))}
@@ -225,24 +207,22 @@ const SearchResultsList = ({
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
alignSelf: "center",
alignSelf: 'center',
}}
>
{"Chargement…"}
{'Chargement…'}
</Text>
)}
</>
)}
{playbacks.length === 0 && !playbacksLoading && (
<EmptyText text={"Aucun playback trouvé"} />
<EmptyText text={'Aucun playback trouvé'} />
)}
</View>
)}
{shouldShowUsers && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<CreateLyricsHeader gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}>
{users.length > 0 && (
<View style={{ gap: 8 }}>
{Array.isArray(users) &&
@@ -255,7 +235,7 @@ const SearchResultsList = ({
<ProfilePicture
uri={user?.profilePictureURL || defaultAvatar}
size={60}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
<Text
style={{
@@ -264,7 +244,7 @@ const SearchResultsList = ({
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{getArtistDisplayName(user, "Utilisateur")}
{getArtistDisplayName(user, 'Utilisateur')}
</Text>
</Pressable>
))}
@@ -274,26 +254,22 @@ const SearchResultsList = ({
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
alignSelf: "center",
alignSelf: 'center',
}}
>
{"Chargement…"}
{'Chargement…'}
</Text>
)}
</View>
)}
{users.length === 0 && !usersLoading && (
<EmptyText text={"Aucun profil trouvé"} />
)}
{users.length === 0 && !usersLoading && <EmptyText text={'Aucun profil trouvé'} />}
</CreateLyricsHeader>
</View>
)}
{selected === "Clips" && (
{selected === 'Clips' && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<EmptyText text={"Aucun clip trouvé"} />
<CreateLyricsHeader gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}>
<EmptyText text={'Aucun clip trouvé'} />
</CreateLyricsHeader>
</View>
)}
@@ -307,7 +283,7 @@ const SearchResultsList = ({
projectId={selectedProjectId}
/>
</View>
);
};
)
}
export default SearchResultsList;
export default SearchResultsList
+86 -100
View File
@@ -1,33 +1,33 @@
/* eslint-disable react/display-name */
import * as AppleAuthentication from "expo-apple-authentication";
import React, { useCallback, useState } from "react";
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
import Svg, { Path } from "react-native-svg";
import { useGlobal } from "reactn";
import { handleFirebaseError } from "../actions/signupActions.js";
import { background } from "../assets";
import GradientButton from "../components/GradientButton.js";
import { Input } from "../components/Input.js";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import firebase from "../config/firebase";
import useSocialAuth from "../hooks/useSocialAuth";
import { isWeb } from "../hooks/useLayoutType";
import Page from "../layouts/Page.js";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService.js";
import { FONT_FAMILY } from "../styles/Fonts.js";
import Palette from "../styles/Palette.js";
import * as AppleAuthentication from 'expo-apple-authentication'
import React, { useCallback, useState } from 'react'
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native'
import Svg, { Path } from 'react-native-svg'
import { useGlobal } from 'reactn'
import { handleFirebaseError } from '../actions/signupActions.js'
import { background } from '../assets'
import GradientButton from '../components/GradientButton.js'
import { Input } from '../components/Input.js'
import ItemContainer from '../components/ItemContainer/ItemContainer'
import firebase from '../config/firebase'
import useSocialAuth from '../hooks/useSocialAuth'
import { isWeb } from '../hooks/useLayoutType'
import Page from '../layouts/Page.js'
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService.js'
import { FONT_FAMILY } from '../styles/Fonts.js'
import Palette from '../styles/Palette.js'
export default ({ navigation }) => {
const [, setTooltip] = useGlobal("_tooltip");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal('_tooltip')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const afterLoginNavigate = useCallback(async () => {
const uid = firebase.auth().currentUser?.uid;
if (!uid) throw new Error("Aucun utilisateur après connexion");
navigation.reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
}, [navigation]);
const uid = firebase.auth().currentUser?.uid
if (!uid) throw new Error('Aucun utilisateur après connexion')
navigation.reset({ index: 0, routes: [{ name: Routes.BottomTab }] })
}, [navigation])
const {
signInWithGoogle,
@@ -36,47 +36,42 @@ export default ({ navigation }) => {
isLoading: socialLoading,
} = useSocialAuth({
onSuccess: afterLoginNavigate,
});
})
const isBusy = loading || socialLoading;
const isBusy = loading || socialLoading
const onLogin = async () => {
try {
setLoading(true);
await firebase.auth().signInWithEmailAndPassword(email.trim(), password);
await afterLoginNavigate();
setLoading(true)
await firebase.auth().signInWithEmailAndPassword(email.trim(), password)
await afterLoginNavigate()
} catch (e) {
console.log("Login error", e?.message);
console.log('Login error', e?.message)
const message =
e?.code && e.code.startsWith("auth/")
e?.code && e.code.startsWith('auth/')
? handleFirebaseError(e.code)
: e?.message || "Connexion impossible";
setTooltip({ text: message, type: "error" });
: e?.message || 'Connexion impossible'
setTooltip({ text: message, type: 'error' })
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
width={isWeb ? 600 : null}
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
headerType="NAVIGATION"
title={isWeb ? "" : "Connexion"}
title={isWeb ? '' : 'Connexion'}
hideBackButton
>
<View style={styles.screen}>
<ItemContainer
height={isWeb ? 520 : 550}
disableKeyboardHeight
style={styles.loginCard}
>
<ItemContainer height={isWeb ? 520 : 550} disableKeyboardHeight style={styles.loginCard}>
<View style={styles.cardContent}>
<View style={styles.greetingSection}>
<Text style={styles.greetingTitle}>{"Bonjour !"}</Text>
<Text style={styles.greetingTitle}>{'Bonjour !'}</Text>
<Text style={styles.greetingSubtitle}>
Nous sommes heureux de te revoir parmi nous. Prêt·e à reprendre
le rythme ?
Nous sommes heureux de te revoir parmi nous. Prêt·e à reprendre le rythme ?
</Text>
</View>
<View style={styles.formFields}>
@@ -99,10 +94,7 @@ export default ({ navigation }) => {
/>
<Pressable
onPress={() => navigate(Routes.ForgotPassword)}
style={({ pressed }) => [
styles.linkPressable,
pressed && styles.linkPressed,
]}
style={({ pressed }) => [styles.linkPressable, pressed && styles.linkPressed]}
>
<Text style={styles.linkText}>Mot de passe oublié ?</Text>
</Pressable>
@@ -111,8 +103,8 @@ export default ({ navigation }) => {
<GradientButton
title="Se connecter"
containerStyle={{
width: "90%",
alignSelf: "center",
width: '90%',
alignSelf: 'center',
}}
onPress={onLogin}
disabled={isBusy || !email || !password}
@@ -131,37 +123,31 @@ export default ({ navigation }) => {
loading={socialLoading}
/>
</View>
</View>
</ItemContainer>
<View style={styles.bottomFooter}>
<Text style={styles.bottomFooterText}>
Tu n'as pas de compte ?{" "}
<Text
style={styles.bottomFooterLink}
onPress={() => navigate(Routes.Register)}
>
Tu n'as pas de compte ?{' '}
<Text style={styles.bottomFooterLink} onPress={() => navigate(Routes.Register)}>
Créer un compte
</Text>
</Text>
</View>
</View>
</Page>
);
};
)
}
const AppleAuthButton = ({ onPress, disabled, loading }) => {
if (isWeb) {
return null;
return null
}
return (
<View style={styles.nativeButtonWrapper}>
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={
AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
}
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.WHITE}
style={styles.nativeButton}
cornerRadius={12}
onPress={onPress}
@@ -173,8 +159,8 @@ const AppleAuthButton = ({ onPress, disabled, loading }) => {
</View>
) : null}
</View>
);
};
)
}
const GoogleAuthButton = ({ onPress, disabled, loading }) => (
<Pressable
@@ -199,28 +185,28 @@ const GoogleAuthButton = ({ onPress, disabled, loading }) => (
)}
</View>
</Pressable>
);
)
const styles = StyleSheet.create({
screen: {
flex: 1,
width: "100%",
justifyContent: "center",
alignItems: "center",
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
loginCard: {
width: "100%",
width: '100%',
maxWidth: 520,
},
cardContent: {
gap: 28,
paddingTop: 8,
paddingHorizontal: 5,
width: "100%",
width: '100%',
},
greetingSection: {
gap: 6,
width: "100%",
width: '100%',
},
greetingTitle: {
fontSize: 22,
@@ -235,14 +221,14 @@ const styles = StyleSheet.create({
},
formFields: {
gap: 20,
width: "100%",
width: '100%',
},
passwordField: {
gap: 8,
width: "100%",
width: '100%',
},
linkPressable: {
alignSelf: "flex-end",
alignSelf: 'flex-end',
},
linkPressed: {
opacity: 0.6,
@@ -251,7 +237,7 @@ const styles = StyleSheet.create({
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "right",
textAlign: 'right',
},
bottomFooter: {
marginTop: 16,
@@ -261,55 +247,55 @@ const styles = StyleSheet.create({
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
},
bottomFooterLink: {
fontFamily: FONT_FAMILY.InterSemiBold,
color: Palette.white,
},
socialWrapper: {
width: "100%",
width: '100%',
gap: 16,
alignItems: "center",
alignItems: 'center',
},
socialButton: {
alignSelf: "center",
alignSelf: 'center',
borderRadius: 12,
width: "90%",
width: '90%',
backgroundColor: Palette.white,
borderWidth: 1,
borderColor: "#E3E7EF",
borderColor: '#E3E7EF',
paddingVertical: 12,
paddingHorizontal: 20,
shadowColor: "rgba(12, 6, 34, 0.08)",
shadowColor: 'rgba(12, 6, 34, 0.08)',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 2,
},
socialButtonPressed: {
backgroundColor: "#F4F7FF",
backgroundColor: '#F4F7FF',
},
socialButtonDisabled: {
opacity: 0.7,
},
socialButtonContent: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
},
socialIconContainer: {
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
width: 28,
height: 28,
},
socialButtonText: {
flex: 1,
textAlign: "center",
textAlign: 'center',
fontSize: 15,
color: "#202124",
color: '#202124',
fontFamily: FONT_FAMILY.InterSemiBold,
},
socialRightSpacer: {
@@ -317,21 +303,21 @@ const styles = StyleSheet.create({
height: 24,
},
nativeButtonWrapper: {
alignSelf: "center",
width: "90%",
alignSelf: 'center',
width: '90%',
height: 50,
justifyContent: "center",
justifyContent: 'center',
},
nativeButton: {
width: "100%",
width: '100%',
height: 50,
},
nativeLoadingOverlay: {
...StyleSheet.absoluteFillObject,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
});
})
const GoogleLogo = () => (
<Svg width={24} height={24} viewBox="0 0 18 18">
@@ -352,4 +338,4 @@ const GoogleLogo = () => (
d="M9 3.54c1.32 0 2.5.45 3.43 1.33l2.57-2.57C13.47.9 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.02 2.32A5.31 5.31 0 0 1 9 3.54Z"
/>
</Svg>
);
)
+62 -76
View File
@@ -1,100 +1,94 @@
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { BlurView } from "expo-blur";
import React, { useCallback, useMemo } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { ai, background, cardsImg } from "../assets";
import Page from "../layouts/Page";
import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider";
import { getCreationStageStates, getStageAction } from "../utils/projectStages";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import palette from "../styles/Palette";
import FontAwesome from '@expo/vector-icons/FontAwesome'
import { BlurView } from 'expo-blur'
import React, { useCallback, useMemo } from 'react'
import { Image, Platform, Pressable, Text, View } from 'react-native'
import { ai, background, cardsImg } from '../assets'
import Page from '../layouts/Page'
import { navigate } from '../navigation/NavigationService'
import { useUserData } from '../providers/UserDataProvider'
import { getCreationStageStates, getStageAction } from '../utils/projectStages'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import palette from '../styles/Palette'
const CREATE_DATA = [
{
stageKey: "songwriter",
stageKey: 'songwriter',
img: ai.nathalie,
bg: background.writingBG,
label: "Nathalie",
desc: "Let’s write lyrics together !",
type: "Songwriter",
label: 'Nathalie',
desc: 'Let’s write lyrics together !',
type: 'Songwriter',
},
{
stageKey: "beatmaker",
stageKey: 'beatmaker',
img: ai.theo,
bg: background.studioBG,
label: "Theo",
desc: "Come back, when\nyou’ll have lyrics!",
type: "Beatmaker",
label: 'Theo',
desc: 'Come back, when\nyou’ll have lyrics!',
type: 'Beatmaker',
},
{
stageKey: "director",
stageKey: 'director',
img: ai.john,
bg: background.playbackBG,
label: "Theo",
label: 'Theo',
desc: "Theo t'accompagne pour créer ton playback.",
type: "Director",
type: 'Director',
},
{
stageKey: "publisher",
stageKey: 'publisher',
img: cardsImg.production,
bg: background.productionBG2,
label: "Publication",
desc: "Publions ta vidéo sur YouTube !",
type: "Producteur",
label: 'Publication',
desc: 'Publions ta vidéo sur YouTube !',
type: 'Producteur',
},
];
]
const NewMusicOptions = () => {
const { selectedProject } = useUserData();
const stageStates = useMemo(
() => getCreationStageStates(selectedProject),
[selectedProject],
);
const { selectedProject } = useUserData()
const stageStates = useMemo(() => getCreationStageStates(selectedProject), [selectedProject])
const stageStateByKey = useMemo(() => {
return stageStates.reduce((acc, stage) => {
acc[stage.key] = stage;
return acc;
}, {});
}, [stageStates]);
acc[stage.key] = stage
return acc
}, {})
}, [stageStates])
const onPressOption = useCallback(
(stageKey) => {
const stageState = stageStateByKey[stageKey];
const stageState = stageStateByKey[stageKey]
if (!stageState || stageState.isLocked) {
return;
return
}
const action = getStageAction(stageKey, selectedProject);
const action = getStageAction(stageKey, selectedProject)
if (!action?.route) {
return;
return
}
navigate(action.route, action.params);
navigate(action.route, action.params)
},
[stageStateByKey, selectedProject],
);
[stageStateByKey, selectedProject]
)
return (
<Page
backgroundImg={background.homeBG}
headerType="NAVIGATION"
title={!!selectedProject ? "Continuer la création" : "Nouvelle musique"}
title={!!selectedProject ? 'Continuer la création' : 'Nouvelle musique'}
scrollEnabled={true}
>
<View style={{ gap: 10 }}>
{CREATE_DATA.map((item) => {
const locked = stageStateByKey[item.stageKey]?.isLocked ?? true;
const locked = stageStateByKey[item.stageKey]?.isLocked ?? true
return (
<Pressable
key={item.stageKey}
onPress={() => onPressOption(item.stageKey)}
>
<Pressable key={item.stageKey} onPress={() => onPressOption(item.stageKey)}>
<BlurView
tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
position: "absolute",
position: 'absolute',
zIndex: 1,
left: 110,
top: 10,
@@ -102,7 +96,7 @@ const NewMusicOptions = () => {
paddingHorizontal: 12,
borderRadius: 14,
borderBottomLeftRadius: 0,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
}}
// experimentalBlurMethod={
@@ -131,31 +125,27 @@ const NewMusicOptions = () => {
<Image
source={item.bg}
style={{
width: "100%",
width: '100%',
height: 152,
borderRadius: 20,
position: "absolute",
position: 'absolute',
bottom: 0,
}}
/>
<Image
source={item.img}
style={{ width: 130, height: 200 }}
resizeMode="contain"
/>
<Image source={item.img} style={{ width: 130, height: 200 }} resizeMode="contain" />
{locked && (
<View
pointerEvents="none"
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.35)",
backgroundColor: 'rgba(0,0,0,0.35)',
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
}}
>
<View
@@ -163,25 +153,21 @@ const NewMusicOptions = () => {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.4)",
alignItems: "center",
justifyContent: "center",
backgroundColor: 'rgba(0,0,0,0.4)',
alignItems: 'center',
justifyContent: 'center',
}}
>
<FontAwesome
name="lock"
size={24}
color={palette.primary}
/>
<FontAwesome name="lock" size={24} color={palette.primary} />
</View>
</View>
)}
</Pressable>
);
)
})}
</View>
</Page>
);
};
)
}
export default NewMusicOptions;
export default NewMusicOptions
+14 -20
View File
@@ -1,34 +1,28 @@
import React from "react";
import { Text, View } from "react-native";
import Button from "../components/Button";
import { gutters } from "../styles";
import { navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
import { LinearGradient } from "../components/LinearGradient/LinearGradient";
import GradientButton from "../components/GradientButton";
import React from 'react'
import { Text, View } from 'react-native'
import Button from '../components/Button'
import { gutters } from '../styles'
import { navigate } from '../navigation/NavigationService'
import { Routes } from '../navigation'
import { LinearGradient } from '../components/LinearGradient/LinearGradient'
import GradientButton from '../components/GradientButton'
export default ({ navigation }) => {
return (
<View
style={{
flex: 1,
justifyContent: "flex-end",
justifyContent: 'flex-end',
padding: gutters,
paddingBottom: gutters * 2,
backgroundColor: "#0A0D12",
backgroundColor: '#0A0D12',
gap: 10,
}}
>
<GradientButton title="LOGIN" onPress={() => navigate(Routes.Login)} />
<GradientButton
title="WRITING"
onPress={() => navigate(Routes.Writing)}
/>
<GradientButton title="WRITING" onPress={() => navigate(Routes.Writing)} />
<GradientButton title="STUDIO" onPress={() => navigate(Routes.Studio)} />
<GradientButton
title="PRODUCTION"
onPress={() => navigate(Routes.ProductionOnboarding)}
/>
<GradientButton title="PRODUCTION" onPress={() => navigate(Routes.ProductionOnboarding)} />
</View>
);
};
)
}
+22 -29
View File
@@ -1,19 +1,19 @@
import { View, Text } from "react-native";
import React, { useState } from "react";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import GradientButton from "../../components/GradientButton";
import { CHOOSE_DECOR } from "../../data/data";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Routes } from "../../navigation";
import { View, Text } from 'react-native'
import React, { useState } from 'react'
import Page from '../../layouts/Page'
import { background } from '../../assets'
import MusicLandHeader from '../../components/MusicLandHeader'
import { goBack, navigate } from '../../navigation/NavigationService'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import ItemContainer from '../../components/ItemContainer/ItemContainer'
import GradientButton from '../../components/GradientButton'
import { CHOOSE_DECOR } from '../../data/data'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { Routes } from '../../navigation'
const ChooseDecor = () => {
const [selected, setSelected] = useState("");
const [selected, setSelected] = useState('')
return (
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
@@ -27,7 +27,7 @@ const ChooseDecor = () => {
<ItemContainer height={402}>
<View style={{ gap: 10, marginTop: 5, paddingHorizontal: 5 }}>
{CHOOSE_DECOR.map((item, index) => {
const selectedItem = selected === item;
const selectedItem = selected === item
return (
<CreateLyricsHeader
@@ -39,12 +39,8 @@ const ChooseDecor = () => {
borderRadius: 14,
}}
onPress={() => setSelected(item)}
colors={
selectedItem
? ["#99999900", "#FFFFFF"]
: [Palette.tran, Palette.tran]
}
tint={selectedItem ? "default" : "dark"}
colors={selectedItem ? ['#99999900', '#FFFFFF'] : [Palette.tran, Palette.tran]}
tint={selectedItem ? 'default' : 'dark'}
>
<View style={{ paddingVertical: 6 }}>
<Text
@@ -58,18 +54,15 @@ const ChooseDecor = () => {
</Text>
</View>
</CreateLyricsHeader>
);
)
})}
</View>
</ItemContainer>
</View>
<GradientButton
title="Suivant"
onPress={() => navigate(Routes.CreatingDecor)}
/>
<GradientButton title="Suivant" onPress={() => navigate(Routes.CreatingDecor)} />
</View>
</Page>
);
};
)
}
export default ChooseDecor;
export default ChooseDecor
+40 -40
View File
@@ -1,33 +1,33 @@
import { View, Text, Image, Platform } from "react-native";
import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page";
import { ai, background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import { Palette, Style } from "../../styles";
import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../../styles/Fonts";
import ProgressBar from "../../components/ProgressBar";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
import { View, Text, Image, Platform } from 'react-native'
import React, { useEffect, useState } from 'react'
import Page from '../../layouts/Page'
import { ai, background } from '../../assets'
import MusicLandHeader from '../../components/MusicLandHeader'
import { goBack, navigate } from '../../navigation/NavigationService'
import { Palette, Style } from '../../styles'
import { BlurView } from 'expo-blur'
import { FONT_FAMILY } from '../../styles/Fonts'
import ProgressBar from '../../components/ProgressBar'
import { Routes } from '../../navigation'
import GradientButton from '../../components/GradientButton'
const CreatingDecor = () => {
const [progress, setProgress] = useState(0);
const [progress, setProgress] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setProgress((prevProgress) => {
if (prevProgress >= 100) {
clearInterval(interval);
clearInterval(interval)
return 100;
return 100
}
return prevProgress + 1;
});
}, 100);
return prevProgress + 1
})
}, 100)
return () => clearInterval(interval);
}, []);
return () => clearInterval(interval)
}, [])
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
@@ -41,23 +41,23 @@ const CreatingDecor = () => {
>
<View
style={{
width: "100%",
height: "50%",
width: '100%',
height: '50%',
}}
>
<View
style={{
zIndex: 1,
position: "absolute",
position: 'absolute',
top: -150,
width: "50%",
height: "80%",
alignSelf: "center",
width: '50%',
height: '80%',
alignSelf: 'center',
}}
>
<Image
source={ai.john}
style={{ width: "100%", height: "100%", right: -10 }}
style={{ width: '100%', height: '100%', right: -10 }}
resizeMode="contain"
/>
</View>
@@ -65,42 +65,42 @@ const CreatingDecor = () => {
style={{
flex: 1,
borderRadius: 20,
overflow: "hidden",
backgroundColor: "#0F0C1933",
overflow: 'hidden',
backgroundColor: '#0F0C1933',
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
intensity={Platform.OS !== 'ios' ? 10 : 40}
tint="dark"
style={{ flex: 1, padding: 10, paddingBottom: 20 }}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
<View style={{ flex: 1, justifyContent: 'flex-end', gap: 20 }}>
<View>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Ton décor est en cours de{"\n"}création
Ton décor est en cours de{'\n'}création
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Veuillez patienter
</Text>
</View>
<View style={{ alignItems: "center", gap: 16 }}>
<View style={{ alignItems: 'center', gap: 16 }}>
<ProgressBar gradient progress={progress} />
<Text
style={{
@@ -115,8 +115,8 @@ const CreatingDecor = () => {
<GradientButton
title="Découvrir mon Playback"
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={() => navigate(Routes.VideoFinalize)}
/>
@@ -126,7 +126,7 @@ const CreatingDecor = () => {
</View>
</View>
</Page>
);
};
)
}
export default CreatingDecor;
export default CreatingDecor
+37 -44
View File
@@ -1,51 +1,48 @@
import React, { useCallback, useState } from "react";
import { Image, StyleSheet, View } from "react-native";
import { ai, background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import Page from "../../layouts/Page";
import { isWeb } from "../../hooks/useLayoutType";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import React, { useCallback, useState } from 'react'
import { Image, StyleSheet, View } from 'react-native'
import { ai, background } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page'
import { isWeb } from '../../hooks/useLayoutType'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters } from '../../styles'
const Playback = ({ route, navigation }) => {
const { project } = route.params || {};
const { videos } = useUser();
const { project } = route.params || {}
const { videos } = useUser()
// const benhaiUrl = isWeb ? videos?.benhaiWeb || null : videos?.benhai;
const theoUrl = isWeb ? videos?.theoWeb : videos?.theo;
const [showIntro, setShowIntro] = useState(theoUrl);
const theoUrl = isWeb ? videos?.theoWeb : videos?.theo
const [showIntro, setShowIntro] = useState(theoUrl)
const handleCloseIntro = () => {
setShowIntro(null);
};
setShowIntro(null)
}
const onPressRecord = useCallback(() => {
navigate(Routes.RecordPlayback, { project });
}, [project]);
navigate(Routes.RecordPlayback, { project })
}, [project])
return (
<Page headerType="NONE" backgroundImg={isWeb ? background.playbackWeb : background.playbackMobile}>
<Page
headerType="NONE"
backgroundImg={isWeb ? background.playbackWeb : background.playbackMobile}
>
{/* <Image source={ai.theo} style={styles.img} resizeMode="contain" /> */}
<MusicLandHeader
onPressBack={() => navigation.navigate(Routes.Home)}
progress={25}
/>
<MusicLandHeader onPressBack={() => navigation.navigate(Routes.Home)} progress={25} />
<View
style={{
flex: 1,
paddingBottom: gutters * 2,
justifyContent: "flex-end",
justifyContent: 'flex-end',
}}
>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<GradientButton
title="Enregistrer mon Playback"
onPress={onPressRecord}
/>
<View style={{ width: '80%', alignSelf: 'center', gap: 12 }}>
<GradientButton title="Enregistrer mon Playback" onPress={onPressRecord} />
<BorderGradientButton
title="Guide du Playbacker"
onPress={() => navigate(Routes.PlaybackGuide)}
@@ -53,22 +50,18 @@ const Playback = ({ route, navigation }) => {
{/* <BorderGradientButton title="Importer une vidéo" /> */}
</View>
</View>
<FullscreenIntroVideo
url={showIntro}
visible={showIntro}
onClose={handleCloseIntro}
/>
<FullscreenIntroVideo url={showIntro} visible={showIntro} onClose={handleCloseIntro} />
</Page>
);
};
)
}
export default Playback;
export default Playback
const styles = StyleSheet.create({
img: {
width: "100%",
height: "70%",
position: "absolute",
width: '100%',
height: '70%',
position: 'absolute',
bottom: -40,
},
});
})
+330 -312
View File
@@ -1,341 +1,359 @@
import React from "react";
import { ScrollView, StyleSheet, Text, View, Platform } from "react-native";
import { BlurView } from "expo-blur";
import { background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { isWeb } from "../../hooks/useLayoutType";
import React from 'react'
import { ScrollView, StyleSheet, Text, View, Platform } from 'react-native'
import { BlurView } from 'expo-blur'
import { background } from '../../assets'
import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { isWeb } from '../../hooks/useLayoutType'
const PlaybackGuide = ({ navigation }) => {
return (
<Page headerType="NONE" backgroundImg={isWeb ? background.playbackWeb : background.playbackMobile}>
<MusicLandHeader onPressBack={() => navigation.goBack()} />
<View style={styles.container}>
<BlurView intensity={Platform.OS === "ios" ? 80 : 40} style={styles.blurContainer} experimentalBlurMethod="dimezisBlurView">
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<Text style={styles.title}>Le Guide Ultime du Playback de TA Chanson : L'Expérience Créative Totale</Text>
return (
<Page
headerType="NONE"
backgroundImg={isWeb ? background.playbackWeb : background.playbackMobile}
>
<MusicLandHeader onPressBack={() => navigation.goBack()} />
<View style={styles.container}>
<BlurView
intensity={Platform.OS === 'ios' ? 80 : 40}
style={styles.blurContainer}
experimentalBlurMethod="dimezisBlurView"
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<Text style={styles.title}>
Le Guide Ultime du Playback de TA Chanson : L'Expérience Créative Totale
</Text>
<Text style={styles.sectionTitle}>Introduction : Tu Es l'Artiste ET le Performeur !</Text>
<Text style={styles.text}>
Imagine ça : tu as créé Ta chanson, TES paroles, TA mélodie, TON univers.
Et maintenant, tu vas lui donner vie en playback. C'est une expérience UNIQUE au monde !
Tu n'imites personne - tu deviens la version scénique de toi-même. C'est comme avoir un
jumeau qui chante pendant que toi, tu performes. Double dose de créativité, double dose de
fun !
</Text>
<Text style={styles.sectionTitle}>
Introduction : Tu Es l'Artiste ET le Performeur !
</Text>
<Text style={styles.text}>
Imagine ça : tu as créé Ta chanson, TES paroles, TA mélodie, TON univers. Et
maintenant, tu vas lui donner vie en playback. C'est une expérience UNIQUE au monde !
Tu n'imites personne - tu deviens la version scénique de toi-même. C'est comme avoir
un jumeau qui chante pendant que toi, tu performes. Double dose de créativité, double
dose de fun !
</Text>
<Text style={styles.chapterTitle}>Chapitre 1 : Le Lip-Sync de Ta Propre Création - L'Avantage Ultime</Text>
<Text style={styles.chapterTitle}>
Chapitre 1 : Le Lip-Sync de Ta Propre Création - L'Avantage Ultime
</Text>
<Text style={styles.subTitle}>Le Pouvoir d'Être le Créateur</Text>
<Text style={styles.text}>
Tu as un super-pouvoir que personne d'autre n'a : tu connais CHAQUE intention derrière
chaque mot, chaque respiration, chaque silence. Tu n'as pas à deviner ce que l'artiste
voulait dire - c'est TOI l'artiste ! Cette connexion intime avec ta chanson va transparaître
dans chaque mouvement de tes lèvres.
</Text>
<Text style={styles.subTitle}>Le Pouvoir d'Être le Créateur</Text>
<Text style={styles.text}>
Tu as un super-pouvoir que personne d'autre n'a : tu connais CHAQUE intention derrière
chaque mot, chaque respiration, chaque silence. Tu n'as pas à deviner ce que l'artiste
voulait dire - c'est TOI l'artiste ! Cette connexion intime avec ta chanson va
transparaître dans chaque mouvement de tes lèvres.
</Text>
<Text style={styles.subTitle}>La Règle des 15 Écoutes - Version Créateur</Text>
<Text style={styles.text}>
Oui, même pour TA chanson, les 15 écoutes sont cruciales ! Mais avec une twist magique :
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 1-5 : Redécouvre Ta Création</Text> Écoute ta chanson comme si c'était la première
fois. Tu vas être surpris de détails que tu avais oubliés !
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 6-10 : L'Analyse du Créateur</Text>
{"\n"}• Où as-tu placé les émotions fortes ?
{"\n"}• Quels mots méritent d'être sur-articulés ?
{"\n"}• Où sont tes respirations naturelles dans l'enregistrement ?
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 11-15 : La Transformation</Text> Maintenant, détache-toi du "toi qui a enregistré" pour
devenir le "toi qui performe". C'est là que la magie opère !
</Text>
<Text style={styles.subTitle}>La Règle des 15 Écoutes - Version Créateur</Text>
<Text style={styles.text}>
Oui, même pour TA chanson, les 15 écoutes sont cruciales ! Mais avec une twist magique
:
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 1-5 : Redécouvre Ta Création</Text> Écoute ta
chanson comme si c'était la première fois. Tu vas être surpris de détails que tu avais
oubliés !
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 6-10 : L'Analyse du Créateur</Text>
{'\n'}• Où as-tu placé les émotions fortes ?{'\n'}• Quels mots méritent d'être
sur-articulés ?{'\n'}• Où sont tes respirations naturelles dans l'enregistrement ?
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 11-15 : La Transformation</Text> Maintenant,
détache-toi du "toi qui a enregistré" pour devenir le "toi qui performe". C'est là que
la magie opère !
</Text>
<Text style={styles.subTitle}>Les Secrets du Créateur-Performeur</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Phénomène de Dédoublement</Text> C'est normal de se sentir bizarre au début - tu entends
ta voix mais tu ne chantes pas ! Embrasse cette sensation unique. C'est comme être ton
propre fan numéro un.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>La Liberté de Réinterpréter</Text> Tu as le droit de changer l'émotion ! Si tu as enregistré la
chanson en étant mélancolique mais qu'aujourd'hui tu la sens joyeuse - VAS-Y ! C'est TA
chanson, tu peux la réinventer à chaque performance.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Anticipation Parfaite</Text> Tu connais chaque micro-pause, chaque inflexion. Utilise cet
avantage ! Anticipe les moments forts avec ton expression faciale une fraction de seconde
avant qu'ils arrivent.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Truc du "Nouveau Moi"</Text> Crée un personnage scénique de toi-même. Peut-être que le
"toi studio" est introverti, mais le "toi playback" est extraverti. Cette dualité est fascinante et
totalement unique à ton expérience !
</Text>
<Text style={styles.subTitle}>Les Secrets du Créateur-Performeur</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Phénomène de Dédoublement</Text> C'est normal de se
sentir bizarre au début - tu entends ta voix mais tu ne chantes pas ! Embrasse cette
sensation unique. C'est comme être ton propre fan numéro un.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>La Liberté de Réinterpréter</Text> Tu as le droit de changer
l'émotion ! Si tu as enregistré la chanson en étant mélancolique mais qu'aujourd'hui
tu la sens joyeuse - VAS-Y ! C'est TA chanson, tu peux la réinventer à chaque
performance.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Anticipation Parfaite</Text> Tu connais chaque
micro-pause, chaque inflexion. Utilise cet avantage ! Anticipe les moments forts avec
ton expression faciale une fraction de seconde avant qu'ils arrivent.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Truc du "Nouveau Moi"</Text> Crée un personnage scénique
de toi-même. Peut-être que le "toi studio" est introverti, mais le "toi playback" est
extraverti. Cette dualité est fascinante et totalement unique à ton expérience !
</Text>
<Text style={styles.chapterTitle}>Chapitre 2 : Capturer l'Essence de Ta Création</Text>
<Text style={styles.chapterTitle}>Chapitre 2 : Capturer l'Essence de Ta Création</Text>
<Text style={styles.subTitle}>Ton Smartphone, Ton Studio Visuel</Text>
<Text style={styles.text}>
Quand tu filmes le playback de TA chanson, tu complètes l'œuvre. La musique était l'audio,
maintenant tu ajoutes le visuel !
</Text>
<Text style={styles.subTitle}>Ton Smartphone, Ton Studio Visuel</Text>
<Text style={styles.text}>
Quand tu filmes le playback de TA chanson, tu complètes l'œuvre. La musique était
l'audio, maintenant tu ajoutes le visuel !
</Text>
<Text style={styles.subTitle}>Les Angles Qui Racontent Ton Histoire</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Confession" (Très Proche)</Text> Pour les parties intimes de ta chanson. Le spectateur
doit sentir que tu lui confies un secret.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Concert" (Plan Large)</Text> Montre l'artiste complet en toi ! Parfait pour les refrains
énergiques où tu veux bouger.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Clip Musical" (Variété)</Text> Change d'angle selon les parties de ta chanson. Verse 1
proche, refrain large, pont en profil... Raconte visuellement l'histoire de ta musique.
</Text>
<Text style={styles.subTitle}>Les Angles Qui Racontent Ton Histoire</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Confession" (Très Proche)</Text> Pour les parties
intimes de ta chanson. Le spectateur doit sentir que tu lui confies un secret.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Concert" (Plan Large)</Text> Montre l'artiste
complet en toi ! Parfait pour les refrains énergiques où tu veux bouger.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Clip Musical" (Variété)</Text> Change d'angle selon
les parties de ta chanson. Verse 1 proche, refrain large, pont en profil... Raconte
visuellement l'histoire de ta musique.
</Text>
<Text style={styles.subTitle}>L'Accessoire Signature : L'Instrument de Ta Création</Text>
<Text style={styles.text}>
C'est encore plus puissant quand c'est TA chanson !
</Text>
<Text style={styles.text}>
Si tu as composé avec un instrument :
{"\n"}• Mime-le même si tu ne l'as pas sous la main
{"\n"}• Montre les accords que TU as choisis
{"\n"}• Tes mains connaissent les mouvements par cœur
</Text>
<Text style={styles.text}>
Si c'est une création digitale :
{"\n"}• Un casque audio autour du cou = le créateur
{"\n"}• Un smartphone en main = le compositeur moderne
{"\n"}• Les mains qui "mixent" dans l'air = le producteur
</Text>
<Text style={styles.text}>
L'accessoire émotionnel :
{"\n"}• Un carnet (tes paroles originales !)
{"\n"}• Un objet qui t'a inspiré la chanson
{"\n"}• Un élément qui représente le thème de ta chanson
</Text>
<Text style={styles.subTitle}>
L'Accessoire Signature : L'Instrument de Ta Création
</Text>
<Text style={styles.text}>C'est encore plus puissant quand c'est TA chanson !</Text>
<Text style={styles.text}>
Si tu as composé avec un instrument :{'\n'}• Mime-le même si tu ne l'as pas sous la
main
{'\n'}• Montre les accords que TU as choisis
{'\n'}• Tes mains connaissent les mouvements par cœur
</Text>
<Text style={styles.text}>
Si c'est une création digitale :{'\n'}• Un casque audio autour du cou = le créateur
{'\n'}• Un smartphone en main = le compositeur moderne
{'\n'}• Les mains qui "mixent" dans l'air = le producteur
</Text>
<Text style={styles.text}>
L'accessoire émotionnel :{'\n'}• Un carnet (tes paroles originales !)
{'\n'}• Un objet qui t'a inspiré la chanson
{'\n'}• Un élément qui représente le thème de ta chanson
</Text>
<Text style={styles.chapterTitle}>Chapitre 3 : La Scène de Ta Première</Text>
<Text style={styles.chapterTitle}>Chapitre 3 : La Scène de Ta Première</Text>
<Text style={styles.subTitle}>Chaque Lieu Devient TON Lieu</Text>
<Text style={styles.text}>
Quand tu performes ta propre chanson, chaque endroit prend une signification spéciale. Ce
n'est pas juste un décor, c'est le lieu de naissance visuelle de ta création !
</Text>
<Text style={styles.subTitle}>Chaque Lieu Devient TON Lieu</Text>
<Text style={styles.text}>
Quand tu performes ta propre chanson, chaque endroit prend une signification spéciale.
Ce n'est pas juste un décor, c'est le lieu de naissance visuelle de ta création !
</Text>
<Text style={styles.subTitle}>Les Scènes Qui Amplifient Ta Création</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Endroit Où Tu l'as Écrite - Le Retour aux Sources</Text> Si tu as écrit ta chanson dans ta
chambre, filme-là ! C'est puissant de montrer l'artiste là où la magie a commencé.
L'authenticité sera palpable.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Studio Maison - Là Où Tu l'as Enregistrée</Text> Montre ton setup, même basique ! Un
micro, un ordinateur, des câbles... C'est TON univers créatif. Les gens adorent voir les
coulisses.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Lieu Qui t'a Inspiré - La Boucle Créative</Text> Tu as écrit sur la plage ? Sur l'amour au café
? Sur la solitude au parc ? Retourne-y ! La cohérence entre ton inspiration et ta performance
est magique.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>La Scène Imaginaire - Ce Que Tu Visualisais</Text> Quand tu as créé cette chanson, où
t'imaginais-tu la performer ? Un grand stade ? Un petit club intime ? Recrée cette ambiance
avec ce que tu as !
</Text>
<Text style={styles.subTitle}>Les Scènes Qui Amplifient Ta Création</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Endroit Où Tu l'as Écrite - Le Retour aux Sources</Text>{' '}
Si tu as écrit ta chanson dans ta chambre, filme-là ! C'est puissant de montrer
l'artiste là où la magie a commencé. L'authenticité sera palpable.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Studio Maison - Là Où Tu l'as Enregistrée</Text> Montre
ton setup, même basique ! Un micro, un ordinateur, des câbles... C'est TON univers
créatif. Les gens adorent voir les coulisses.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Lieu Qui t'a Inspiré - La Boucle Créative</Text> Tu as
écrit sur la plage ? Sur l'amour au café ? Sur la solitude au parc ? Retourne-y ! La
cohérence entre ton inspiration et ta performance est magique.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>La Scène Imaginaire - Ce Que Tu Visualisais</Text> Quand tu
as créé cette chanson, où t'imaginais-tu la performer ? Un grand stade ? Un petit club
intime ? Recrée cette ambiance avec ce que tu as !
</Text>
<Text style={styles.subTitle}>Suggestions Créatives Uniques :</Text>
<Text style={styles.text}>
• La Voiture : Si c'est là que tu as eu l'idée de la mélodie
{"\n"}• La Douche : Avec le téléphone protégé, si c'est là que tu chantes toujours
{"\n"}• Le Toit/Balcon : Pour une sensation de "crier au monde" ta création
{"\n"}• Devant le Miroir : Le dialogue entre toi créateur et toi performeur
{"\n"}• Dans le Noir avec une Seule Lumière : Pour l'aspect dramatique et intime
</Text>
<Text style={styles.subTitle}>Suggestions Créatives Uniques :</Text>
<Text style={styles.text}>
• La Voiture : Si c'est là que tu as eu l'idée de la mélodie
{'\n'}• La Douche : Avec le téléphone protégé, si c'est là que tu chantes toujours
{'\n'}• Le Toit/Balcon : Pour une sensation de "crier au monde" ta création
{'\n'}• Devant le Miroir : Le dialogue entre toi créateur et toi performeur
{'\n'}• Dans le Noir avec une Seule Lumière : Pour l'aspect dramatique et intime
</Text>
<Text style={styles.chapterTitle}>Chapitre 4 : Le Mindset du Créateur-Performeur</Text>
<Text style={styles.chapterTitle}>Chapitre 4 : Le Mindset du Créateur-Performeur</Text>
<Text style={styles.subTitle}>L'Expérience Unique au Monde</Text>
<Text style={styles.text}>
Ce que tu fais est EXTRAORDINAIRE. Tu es à la fois Mozart et le pianiste, Beyoncé et la
performeuse, le compositeur ET l'interprète. Cette dualité créative est rare et précieuse !
</Text>
<Text style={styles.subTitle}>L'Expérience Unique au Monde</Text>
<Text style={styles.text}>
Ce que tu fais est EXTRAORDINAIRE. Tu es à la fois Mozart et le pianiste, Beyoncé et
la performeuse, le compositeur ET l'interprète. Cette dualité créative est rare et
précieuse !
</Text>
<Text style={styles.subTitle}>Les Mantras du Créateur en Playback</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"C'est MON Bébé Musical"</Text> Cette chanson est née de toi. Chaque note, chaque mot est ton
enfant créatif. Quand tu la performes en playback, tu lui donnes une nouvelle dimension.
Sois fier !
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"Je Me Réinvente à Chaque Prise"</Text> L'avantage de faire le playback de TA chanson ? Tu
peux explorer 1000 façons de l'interpréter ! Mélancolique le matin, énergique le soir. C'est ta
playground créative.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"Mes Deux Moi Collaborent"</Text> Le toi qui a chanté et le toi qui performe sont une équipe.
C'est une collaboration unique avec toi-même à travers le temps !
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"C'est Mon Histoire, Mes Règles"</Text> Personne ne peut te dire "ce n'est pas comme ça que
l'artiste le fait" - parce que l'artiste, c'est TOI !
</Text>
<Text style={styles.subTitle}>Les Mantras du Créateur en Playback</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"C'est MON Bébé Musical"</Text> Cette chanson est née de
toi. Chaque note, chaque mot est ton enfant créatif. Quand tu la performes en
playback, tu lui donnes une nouvelle dimension. Sois fier !
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"Je Me Réinvente à Chaque Prise"</Text> L'avantage de faire
le playback de TA chanson ? Tu peux explorer 1000 façons de l'interpréter !
Mélancolique le matin, énergique le soir. C'est ta playground créative.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"Mes Deux Moi Collaborent"</Text> Le toi qui a chanté et le
toi qui performe sont une équipe. C'est une collaboration unique avec toi-même à
travers le temps !
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>"C'est Mon Histoire, Mes Règles"</Text> Personne ne peut te
dire "ce n'est pas comme ça que l'artiste le fait" - parce que l'artiste, c'est TOI !
</Text>
<Text style={styles.subTitle}>La Célébration de Ta Création</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Rituel du Créateur :</Text>
{"\n"}1. Écoute ta chanson une fois les yeux fermés - Reconnecte avec ton intention originale
{"\n"}2. Dis à voix haute : "J'ai créé quelque chose qui n'existait pas avant"
{"\n"}3. Danse 30 secondes sur TA création - Célèbre-toi !
{"\n"}4. Filme avec fierté - Ce moment est historique dans ton parcours
</Text>
<Text style={styles.subTitle}>La Célébration de Ta Création</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Rituel du Créateur :</Text>
{'\n'}1. Écoute ta chanson une fois les yeux fermés - Reconnecte avec ton intention
originale
{'\n'}2. Dis à voix haute : "J'ai créé quelque chose qui n'existait pas avant"
{'\n'}3. Danse 30 secondes sur TA création - Célèbre-toi !{'\n'}4. Filme avec fierté -
Ce moment est historique dans ton parcours
</Text>
<Text style={styles.subTitle}>Le Partage de Ton Univers</Text>
<Text style={styles.text}>
Quand tu partages le playback de ta propre chanson, tu offres une expérience complète :
{"\n"}• Les gens découvrent ta musique
{"\n"}• Ils voient ta personnalité
{"\n"}• Ils connectent avec l'artiste complet que tu es
{"\n"}• Ils witness la naissance visuelle de ton art
</Text>
<Text style={styles.subTitle}>Le Partage de Ton Univers</Text>
<Text style={styles.text}>
Quand tu partages le playback de ta propre chanson, tu offres une expérience complète
:{'\n'}• Les gens découvrent ta musique
{'\n'}• Ils voient ta personnalité
{'\n'}• Ils connectent avec l'artiste complet que tu es
{'\n'}• Ils witness la naissance visuelle de ton art
</Text>
<Text style={styles.chapterTitle}>Résumé : Ton Parcours Créatif Unique</Text>
<Text style={styles.chapterTitle}>Résumé : Ton Parcours Créatif Unique</Text>
<Text style={styles.subTitle}>🎵 Chapitre 1 - Ton Lip-Sync Signature</Text>
<Text style={styles.text}>
• Tu connais chaque intention : Avantage ultime du créateur
{"\n"}• 15 écoutes pour redécouvrir : Même ta création mérite cette attention
{"\n"}• Le dédoublement créatif : Embrasse d'être ton propre interprète
{"\n"}• Liberté totale : Réinterprète selon ton humeur du jour
</Text>
<Text style={styles.subTitle}>🎵 Chapitre 1 - Ton Lip-Sync Signature</Text>
<Text style={styles.text}>
• Tu connais chaque intention : Avantage ultime du créateur
{'\n'}• 15 écoutes pour redécouvrir : Même ta création mérite cette attention
{'\n'}• Le dédoublement créatif : Embrasse d'être ton propre interprète
{'\n'}• Liberté totale : Réinterprète selon ton humeur du jour
</Text>
<Text style={styles.subTitle}>🎬 Chapitre 2 - Ton Setup Créatif</Text>
<Text style={styles.text}>
• Les angles qui racontent : Confession, concert, clip
{"\n"}• L'instrument de création : Montre ton processus créatif
{"\n"}• Accessoires significatifs : Objets liés à ton inspiration
{"\n"}• L'authenticité avant tout : Ton setup, tes règles
</Text>
<Text style={styles.subTitle}>🎬 Chapitre 2 - Ton Setup Créatif</Text>
<Text style={styles.text}>
• Les angles qui racontent : Confession, concert, clip
{'\n'}• L'instrument de création : Montre ton processus créatif
{'\n'}• Accessoires significatifs : Objets liés à ton inspiration
{'\n'}• L'authenticité avant tout : Ton setup, tes règles
</Text>
<Text style={styles.subTitle}>🎭 Chapitre 3 - Tes Lieux de Création</Text>
<Text style={styles.text}>
• Retour aux sources : Là où tu as créé
{"\n"}• Le lieu d'inspiration : Boucle ta vision créative
{"\n"}• La scène rêvée : Matérialise ta vision
{"\n"}• Chaque lieu = ton histoire : Personnalise à fond
</Text>
<Text style={styles.subTitle}>🎭 Chapitre 3 - Tes Lieux de Création</Text>
<Text style={styles.text}>
• Retour aux sources : Là où tu as créé
{'\n'}• Le lieu d'inspiration : Boucle ta vision créative
{'\n'}• La scène rêvée : Matérialise ta vision
{'\n'}• Chaque lieu = ton histoire : Personnalise à fond
</Text>
<Text style={styles.subTitle}>🌟 Chapitre 4 - Ton Mindset Unique</Text>
<Text style={styles.text}>
• Double créativité : Compositeur ET performeur
{"\n"}• Fierté créative : Tu as créé quelque chose de rien
{"\n"}• Réinvention constante : Explore toutes les facettes
{"\n"}• Histoire complète : Tu offres l'expérience totale
</Text>
<Text style={styles.subTitle}>🌟 Chapitre 4 - Ton Mindset Unique</Text>
<Text style={styles.text}>
• Double créativité : Compositeur ET performeur
{'\n'}• Fierté créative : Tu as créé quelque chose de rien
{'\n'}• Réinvention constante : Explore toutes les facettes
{'\n'}• Histoire complète : Tu offres l'expérience totale
</Text>
<Text style={styles.chapterTitle}>Ton Défi Ultime : La Première Mondiale !</Text>
<Text style={styles.text}>
Tu es sur le point de faire quelque chose qu'AUCUN autre artiste ne peut faire : créer la
performance visuelle parfaite de TA chanson. C'est ta première mondiale, ton moment
unique !
</Text>
<Text style={styles.text}>
Dans les 24 prochaines heures, donne vie visuellement à ta création. Choisis ta chanson
préférée (parmi les tiennes !), trouve ton spot créatif, et MONTRE au monde non seulement
ta musique, mais TOI en train de la vivre.
</Text>
<Text style={styles.text}>
Rappelle-toi : Tu n'es pas en train d'imiter. Tu n'es pas en train de copier. Tu es en train de
COMPLÉTER ton œuvre. La chanson était le tableau, le playback est le cadre qui le
sublime.
</Text>
<Text style={styles.text}>
C'est ton moment. Ta chanson. Ta performance. Ton histoire.
</Text>
<Text style={styles.text}>
Le monde attend de découvrir l'artiste complet que tu es ! 🎤🎵✨
</Text>
<Text style={styles.text}>
#MonPlaybackMaChanson #CréateurPerformeur #DoubleArtiste
</Text>
<Text style={styles.text}>
P.S. : Garde chaque prise, même les "ratées". Dans 10 ans, tu regarderas ces vidéos et tu
verras le début de ton parcours d'artiste. C'est de l'or pur !
</Text>
</ScrollView>
</BlurView>
</View>
</Page>
);
};
<Text style={styles.chapterTitle}>Ton Défi Ultime : La Première Mondiale !</Text>
<Text style={styles.text}>
Tu es sur le point de faire quelque chose qu'AUCUN autre artiste ne peut faire : créer
la performance visuelle parfaite de TA chanson. C'est ta première mondiale, ton moment
unique !
</Text>
<Text style={styles.text}>
Dans les 24 prochaines heures, donne vie visuellement à ta création. Choisis ta
chanson préférée (parmi les tiennes !), trouve ton spot créatif, et MONTRE au monde
non seulement ta musique, mais TOI en train de la vivre.
</Text>
<Text style={styles.text}>
Rappelle-toi : Tu n'es pas en train d'imiter. Tu n'es pas en train de copier. Tu es en
train de COMPLÉTER ton œuvre. La chanson était le tableau, le playback est le cadre
qui le sublime.
</Text>
<Text style={styles.text}>
C'est ton moment. Ta chanson. Ta performance. Ton histoire.
</Text>
<Text style={styles.text}>
Le monde attend de découvrir l'artiste complet que tu es ! 🎤🎵✨
</Text>
<Text style={styles.text}>
#MonPlaybackMaChanson #CréateurPerformeur #DoubleArtiste
</Text>
<Text style={styles.text}>
P.S. : Garde chaque prise, même les "ratées". Dans 10 ans, tu regarderas ces vidéos et
tu verras le début de ton parcours d'artiste. C'est de l'or pur !
</Text>
</ScrollView>
</BlurView>
</View>
</Page>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: Platform.OS === "web" ? gutters : 0,
marginBottom: gutters,
borderRadius: 20,
overflow: "hidden",
},
blurContainer: {
flex: 1,
backgroundColor: Palette.glass,
},
scrollContent: {
padding: gutters,
paddingBottom: gutters * 3,
},
title: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 24,
color: Palette.white,
textAlign: "center",
marginBottom: 20,
},
sectionTitle: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 20,
color: Palette.primary,
marginBottom: 10,
marginTop: 10,
},
chapterTitle: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 22,
color: Palette.primary,
marginBottom: 15,
marginTop: 25,
textAlign: "center",
},
subTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
marginBottom: 8,
marginTop: 15,
},
text: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 16,
color: Palette.white,
marginBottom: 10,
lineHeight: 24,
},
bold: {
fontFamily: FONT_FAMILY.InterBold,
},
});
container: {
flex: 1,
marginHorizontal: Platform.OS === 'web' ? gutters : 0,
marginBottom: gutters,
borderRadius: 20,
overflow: 'hidden',
},
blurContainer: {
flex: 1,
backgroundColor: Palette.glass,
},
scrollContent: {
padding: gutters,
paddingBottom: gutters * 3,
},
title: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 24,
color: Palette.white,
textAlign: 'center',
marginBottom: 20,
},
sectionTitle: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 20,
color: Palette.primary,
marginBottom: 10,
marginTop: 10,
},
chapterTitle: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 22,
color: Palette.primary,
marginBottom: 15,
marginTop: 25,
textAlign: 'center',
},
subTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
marginBottom: 8,
marginTop: 15,
},
text: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 16,
color: Palette.white,
marginBottom: 10,
lineHeight: 24,
},
bold: {
fontFamily: FONT_FAMILY.InterBold,
},
})
export default PlaybackGuide;
export default PlaybackGuide
+13 -16
View File
@@ -1,11 +1,11 @@
import { View, Text } from "react-native";
import React from "react";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { gutters } from "../../styles";
import GradientButton from "../../components/GradientButton";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import { View, Text } from 'react-native'
import React from 'react'
import Page from '../../layouts/Page'
import { background } from '../../assets'
import { gutters } from '../../styles'
import GradientButton from '../../components/GradientButton'
import { navigate } from '../../navigation/NavigationService'
import { Routes } from '../../navigation'
const PlaybackOnboarding = () => {
return (
@@ -14,16 +14,13 @@ const PlaybackOnboarding = () => {
style={{
flex: 1,
paddingBottom: gutters * 2,
justifyContent: "flex-end",
justifyContent: 'flex-end',
}}
>
<GradientButton
title="Commence"
onPress={() => navigate(Routes.Playback)}
/>
<GradientButton title="Commence" onPress={() => navigate(Routes.Playback)} />
</View>
</Page>
);
};
)
}
export default PlaybackOnboarding;
export default PlaybackOnboarding
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+118 -126
View File
@@ -1,173 +1,167 @@
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import * as FileSystem from "expo-file-system";
import { VideoView, useVideoPlayer } from "expo-video";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Image, Pressable, View } from "react-native";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import ProgressSlider from "../../components/player/ProgressSlider";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer'
import * as FileSystem from 'expo-file-system'
import { VideoView, useVideoPlayer } from 'expo-video'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Pressable, View } from 'react-native'
import { background, icons } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import ProgressSlider from '../../components/player/ProgressSlider'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
const { videoUri, project } = route.params || {}
const songUrl = project?.songUrl || null
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
title: typeof project?.title === "string" ? project.title : "Sans titre",
artist: typeof project?.userName === "string" ? project.userName : "MusicLand",
title: typeof project?.title === 'string' ? project.title : 'Sans titre',
artist: typeof project?.userName === 'string' ? project.userName : 'MusicLand',
artwork: project?.coverUrl || null,
coverUrl: project?.coverUrl || null,
metadata: { projectId: project?.id, screen: "RecordedPlayback" },
});
metadata: { projectId: project?.id, screen: 'RecordedPlayback' },
})
const videoPlayer = useVideoPlayer(videoUri || null, (p) => {
p.loop = false;
p.muted = true; // recorded video has no audio; keep muted anyway
p.timeUpdateEventInterval = 0.2;
});
p.loop = false
p.muted = true // recorded video has no audio; keep muted anyway
p.timeUpdateEventInterval = 0.2
})
const [progressInfo, setProgressInfo] = useState({
pos: 0,
dur: 0,
isPlaying: false,
});
const playbackEndedRef = useRef(false);
})
const playbackEndedRef = useRef(false)
// Start both players on mount
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch (e) { }
if (audioPlayer?.playing) await audioPlayer.pause?.()
} catch (e) {}
try {
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) { }
}, [audioPlayer, videoPlayer]);
if (videoPlayer?.playing) videoPlayer.pause()
} catch (e) {}
}, [audioPlayer, videoPlayer])
useEffect(() => {
playbackEndedRef.current = false;
playbackEndedRef.current = false
const start = async () => {
try {
if (audioPlayer && songUrl) await audioPlayer.play?.();
if (videoPlayer) videoPlayer.play();
} catch (e) { }
};
start();
if (audioPlayer && songUrl) await audioPlayer.play?.()
if (videoPlayer) videoPlayer.play()
} catch (e) {}
}
start()
return () => {
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, songUrl, stopPlayback, videoPlayer]);
playbackEndedRef.current = false
void stopPlayback()
}
}, [audioPlayer, songUrl, stopPlayback, videoPlayer])
// Poll from audio player for progress display; keep video in sync if drifting
useEffect(() => {
const id = global.setInterval(() => {
try {
const dur = (audioPlayer?.duration || 0) * 1000;
const pos = (audioPlayer?.currentTime || 0) * 1000;
const playing = !!audioPlayer?.playing || !!videoPlayer?.playing;
setProgressInfo({ pos, dur, isPlaying: playing });
const dur = (audioPlayer?.duration || 0) * 1000
const pos = (audioPlayer?.currentTime || 0) * 1000
const playing = !!audioPlayer?.playing || !!videoPlayer?.playing
setProgressInfo({ pos, dur, isPlaying: playing })
// basic drift correction: if desync > 300ms, align video
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
const v = (videoPlayer.currentTime || 0) * 1000;
const drift = Math.abs(v - pos);
const v = (videoPlayer.currentTime || 0) * 1000
const drift = Math.abs(v - pos)
if (drift > 350) {
videoPlayer.currentTime = Math.max(0, (pos || 0) / 1000);
videoPlayer.currentTime = Math.max(0, (pos || 0) / 1000)
}
}
} catch (e) { }
}, 250);
return () => global.clearInterval(id);
}, [audioPlayer, videoPlayer]);
} catch (e) {}
}, 250)
return () => global.clearInterval(id)
}, [audioPlayer, videoPlayer])
const onSeek = async (targetMs) => {
try {
const dur = progressInfo.dur || 0;
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)));
if (audioPlayer && dur > 0)
await audioPlayer.seekTo?.(Math.floor(pos / 1000));
if (videoPlayer) videoPlayer.currentTime = Math.max(0, pos / 1000);
} catch (e) { }
};
const dur = progressInfo.dur || 0
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)))
if (audioPlayer && dur > 0) await audioPlayer.seekTo?.(Math.floor(pos / 1000))
if (videoPlayer) videoPlayer.currentTime = Math.max(0, pos / 1000)
} catch (e) {}
}
const onSeekStart = useCallback(() => {
playbackEndedRef.current = false;
}, []);
playbackEndedRef.current = false
}, [])
const pauseDuringSeek = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch (e) { }
if (audioPlayer?.playing) await audioPlayer.pause?.()
} catch (e) {}
try {
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) { }
}, [audioPlayer, videoPlayer]);
if (videoPlayer?.playing) videoPlayer.pause()
} catch (e) {}
}, [audioPlayer, videoPlayer])
const resumeAfterSeek = useCallback(async () => {
try {
if (audioPlayer) await audioPlayer.play?.();
} catch (e) { }
if (audioPlayer) await audioPlayer.play?.()
} catch (e) {}
try {
if (videoPlayer) videoPlayer.play();
} catch (e) { }
}, [audioPlayer, videoPlayer]);
if (videoPlayer) videoPlayer.play()
} catch (e) {}
}, [audioPlayer, videoPlayer])
const sliderProgress = useMemo(() => {
return progressInfo.dur
? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur)
: 0;
}, [progressInfo]);
return progressInfo.dur ? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur) : 0
}, [progressInfo])
useEffect(() => {
const duration = progressInfo?.dur || 0;
if (!duration) return;
const position = progressInfo?.pos || 0;
const duration = progressInfo?.dur || 0
if (!duration) return
const position = progressInfo?.pos || 0
if (playbackEndedRef.current && duration - position > 1000) {
playbackEndedRef.current = false;
return;
playbackEndedRef.current = false
return
}
const remaining = Math.max(0, duration - position);
const remaining = Math.max(0, duration - position)
if (remaining <= 400 && !playbackEndedRef.current) {
playbackEndedRef.current = true;
void stopPlayback();
playbackEndedRef.current = true
void stopPlayback()
}
}, [progressInfo, stopPlayback]);
}, [progressInfo, stopPlayback])
const handleTogglePlayback = async () => {
try {
const duration = progressInfo?.dur || 0;
const position = progressInfo?.pos || 0;
const isAtEnd = duration > 0 && duration - position < 1000;
const isCurrentlyPlaying =
!!audioPlayer?.playing || !!videoPlayer?.playing;
const duration = progressInfo?.dur || 0
const position = progressInfo?.pos || 0
const isAtEnd = duration > 0 && duration - position < 1000
const isCurrentlyPlaying = !!audioPlayer?.playing || !!videoPlayer?.playing
if (isCurrentlyPlaying) {
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
return;
playbackEndedRef.current = false
if (audioPlayer?.playing) await audioPlayer.pause?.()
if (videoPlayer?.playing) videoPlayer.pause()
return
}
if (isAtEnd) {
if (audioPlayer) await audioPlayer.seekTo?.(0);
if (videoPlayer) videoPlayer.currentTime = 0;
if (audioPlayer) await audioPlayer.seekTo?.(0)
if (videoPlayer) videoPlayer.currentTime = 0
}
playbackEndedRef.current = false;
if (songUrl && audioPlayer) await audioPlayer.play?.();
if (videoPlayer) videoPlayer.play();
} catch (e) { }
};
playbackEndedRef.current = false
if (songUrl && audioPlayer) await audioPlayer.play?.()
if (videoPlayer) videoPlayer.play()
} catch (e) {}
}
return (
<Page backgroundColor={Palette.grayMid} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
<View
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}>
<View style={{ flex: 1, gap: 22 }}>
{!!videoUri && (
<VideoView
@@ -175,12 +169,12 @@ const RecordedPlayback = ({ route }) => {
nativeControls={false}
contentFit="contain"
style={{
width: "80%",
width: '80%',
flex: 1,
alignSelf: "center",
alignSelf: 'center',
borderRadius: 16,
backgroundColor: "#00000066",
overflow: "hidden",
backgroundColor: '#00000066',
overflow: 'hidden',
}}
/>
)}
@@ -200,13 +194,13 @@ const RecordedPlayback = ({ route }) => {
width: 72,
height: 72,
borderRadius: 36,
alignSelf: "center",
alignSelf: 'center',
marginTop: 4,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(10, 5, 24, 0.65)",
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(10, 5, 24, 0.65)',
borderWidth: 1,
borderColor: "#F94697",
borderColor: '#F94697',
}}
>
<Image
@@ -215,41 +209,39 @@ const RecordedPlayback = ({ route }) => {
/>
</Pressable>
</View>
<View
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
>
<View style={{ width: '80%', alignSelf: 'center', marginTop: 4, gap: 12 }}>
<GradientButton
title="Je valide"
onPress={() => {
navigate(Routes.PlaybackDownload, {
action: "playback",
action: 'playback',
uri: videoUri,
project,
});
})
}}
/>
<BorderGradientButton
title="Recommencer"
onPress={async () => {
try {
await stopPlayback();
} catch (e) { }
await stopPlayback()
} catch (e) {}
try {
if (videoUri) {
const info = await FileSystem.getInfoAsync(videoUri);
const info = await FileSystem.getInfoAsync(videoUri)
if (info?.exists)
await FileSystem.deleteAsync(videoUri, {
idempotent: true,
});
})
}
} catch (e) { }
navigate(Routes.RecordPlayback, { project });
} catch (e) {}
navigate(Routes.RecordPlayback, { project })
}}
/>
</View>
</View>
</Page>
);
};
)
}
export default RecordedPlayback;
export default RecordedPlayback
+177 -196
View File
@@ -1,266 +1,247 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Image, Pressable, Text, View } from "react-native";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { isWeb } from "../../hooks/useLayoutType";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import { releaseBlobUrl } from "../../utils/blobUrlCache";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import { background, icons } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import Slider from '../../components/Slider'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { isWeb } from '../../hooks/useLayoutType'
import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer'
import { releaseBlobUrl } from '../../utils/blobUrlCache'
const fmtSeconds = (s) => {
const total = Math.max(0, Math.floor(Number(s || 0)));
const m = Math.floor(total / 60).toString();
const sec = (total % 60).toString().padStart(2, "0");
return `${m}:${sec}`;
};
const total = Math.max(0, Math.floor(Number(s || 0)))
const m = Math.floor(total / 60).toString()
const sec = (total % 60).toString().padStart(2, '0')
return `${m}:${sec}`
}
const toSeconds = (value) => {
const n = Number(value ?? 0);
if (!Number.isFinite(n) || n < 0) return 0;
return n > 10000 ? n / 1000 : n; // heuristique ms → s
};
const n = Number(value ?? 0)
if (!Number.isFinite(n) || n < 0) return 0
return n > 10000 ? n / 1000 : n // heuristique ms → s
}
const WEB_PREVIEW_WIDTH = 360;
const WEB_PREVIEW_WIDTH = 360
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
const { videoUri, project } = route.params || {}
const songUrl = project?.songUrl || null
// AUDIO PLAYER (expo-audio → seconds)
const audioPlayer = useSharedAudioPlayer(
songUrl ? { uri: songUrl } : undefined,
{
id: project?.id
? `recorded-${project.id}`
: songUrl
? `recorded-${songUrl}`
: undefined,
title: typeof project?.title === "string" ? project.title : "Sans titre",
artist:
typeof project?.userName === "string" ? project.userName : "MusicLand",
artwork: project?.coverUrl || null,
coverUrl: project?.coverUrl || null,
metadata: { projectId: project?.id, screen: "RecordedPlayback" },
},
);
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
title: typeof project?.title === 'string' ? project.title : 'Sans titre',
artist: typeof project?.userName === 'string' ? project.userName : 'MusicLand',
artwork: project?.coverUrl || null,
coverUrl: project?.coverUrl || null,
metadata: { projectId: project?.id, screen: 'RecordedPlayback' },
})
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
const videoElRef = useRef(null);
const playbackEndedRef = useRef(false);
const shouldPreserveBlobRef = useRef(false);
const videoElRef = useRef(null)
const playbackEndedRef = useRef(false)
const shouldPreserveBlobRef = useRef(false)
const [progress, setProgress] = useState({
posS: 0, // secondes
durS: 0, // secondes
playing: false,
});
})
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch { }
if (audioPlayer?.playing) await audioPlayer.pause?.()
} catch {}
try {
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
videoElRef.current.pause()
}
} catch { }
}, [audioPlayer]);
} catch {}
}, [audioPlayer])
useEffect(() => {
return () => {
if (!shouldPreserveBlobRef.current) {
releaseBlobUrl(videoUri || null);
releaseBlobUrl(videoUri || null)
}
};
}, [videoUri]);
}
}, [videoUri])
// Démarrage / arrêt
useEffect(() => {
playbackEndedRef.current = false;
playbackEndedRef.current = false
const start = async () => {
try {
if (audioPlayer && songUrl) {
await audioPlayer.play?.();
await audioPlayer.play?.()
}
if (videoElRef.current && videoUri) {
// Lecture vidéo HTML5 (muet pour éviter les policies)
videoElRef.current.muted = true;
videoElRef.current.play().catch(() => { });
videoElRef.current.muted = true
videoElRef.current.play().catch(() => {})
}
} catch { }
};
start();
} catch {}
}
start()
return () => {
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, songUrl, stopPlayback, videoUri]);
playbackEndedRef.current = false
void stopPlayback()
}
}, [audioPlayer, songUrl, stopPlayback, videoUri])
// Boucle de progression + éventuelle sync de la vidéo si fournie
useEffect(() => {
const id = setInterval(() => {
try {
const durS = toSeconds(audioPlayer?.duration); // secondes
const posS = toSeconds(audioPlayer?.currentTime); // secondes
const durS = toSeconds(audioPlayer?.duration) // secondes
const posS = toSeconds(audioPlayer?.currentTime) // secondes
setProgress({
posS,
durS,
playing: !!audioPlayer?.playing,
});
})
// Sync vidéo si on a une source vidéo
if (
videoElRef.current &&
videoUri &&
!Number.isNaN(videoElRef.current.currentTime)
) {
const v = Number(videoElRef.current.currentTime || 0);
const drift = Math.abs(v - posS);
if (videoElRef.current && videoUri && !Number.isNaN(videoElRef.current.currentTime)) {
const v = Number(videoElRef.current.currentTime || 0)
const drift = Math.abs(v - posS)
if (drift > 0.35) {
videoElRef.current.currentTime = Math.max(0, posS);
videoElRef.current.currentTime = Math.max(0, posS)
}
}
} catch { }
}, 250);
return () => clearInterval(id);
}, [audioPlayer, videoUri]);
} catch {}
}, 250)
return () => clearInterval(id)
}, [audioPlayer, videoUri])
// Slider: ratio 0..1
const sliderProgress = useMemo(() => {
return progress.durS > 0 ? Math.min(1, progress.posS / progress.durS) : 0;
}, [progress]);
const pendingSeekRef = useRef(null);
return progress.durS > 0 ? Math.min(1, progress.posS / progress.durS) : 0
}, [progress])
const pendingSeekRef = useRef(null)
useEffect(() => {
const duration = progress?.durS || 0;
if (!duration) return;
const position = progress?.posS || 0;
const duration = progress?.durS || 0
if (!duration) return
const position = progress?.posS || 0
if (playbackEndedRef.current && duration - position > 1) {
playbackEndedRef.current = false;
return;
playbackEndedRef.current = false
return
}
const remaining = Math.max(0, duration - position);
const remaining = Math.max(0, duration - position)
if (remaining <= 0.35 && !playbackEndedRef.current) {
playbackEndedRef.current = true;
playbackEndedRef.current = true
void (async () => {
await stopPlayback();
await stopPlayback()
try {
if (audioPlayer) await audioPlayer.seekTo?.(0);
} catch { }
if (audioPlayer) await audioPlayer.seekTo?.(0)
} catch {}
try {
if (videoElRef.current) {
videoElRef.current.currentTime = 0;
videoElRef.current.currentTime = 0
}
} catch { }
})();
} catch {}
})()
}
}, [progress, stopPlayback, audioPlayer]);
}, [progress, stopPlayback, audioPlayer])
const onSeek = useCallback(
async (ratio) => {
const durS = Number(progress.durS || 0);
const target = durS > 0 ? durS * ratio : 0; // secondes
const durS = Number(progress.durS || 0)
const target = durS > 0 ? durS * ratio : 0 // secondes
const promise = (async () => {
try {
if (audioPlayer && durS > 0) {
await audioPlayer.seekTo?.(Math.max(0, target));
await audioPlayer.seekTo?.(Math.max(0, target))
}
if (videoElRef.current && videoUri) {
videoElRef.current.currentTime = Math.max(0, target);
videoElRef.current.currentTime = Math.max(0, target)
}
} catch { }
})();
pendingSeekRef.current = promise;
await promise;
} catch {}
})()
pendingSeekRef.current = promise
await promise
},
[audioPlayer, progress.durS, videoUri],
);
[audioPlayer, progress.durS, videoUri]
)
const waitForPendingSeek = useCallback(async () => {
const promise = pendingSeekRef.current;
pendingSeekRef.current = null;
if (!promise) return;
const promise = pendingSeekRef.current
pendingSeekRef.current = null
if (!promise) return
try {
await promise;
} catch { }
}, []);
await promise
} catch {}
}, [])
const wasPlayingRef = useRef(false);
const isSeekingRef = useRef(false);
const wasPlayingRef = useRef(false)
const isSeekingRef = useRef(false)
const onSeekStart = useCallback(async () => {
try {
if (isSeekingRef.current) return;
isSeekingRef.current = true;
wasPlayingRef.current = !!audioPlayer?.playing;
pendingSeekRef.current = null;
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (isSeekingRef.current) return
isSeekingRef.current = true
wasPlayingRef.current = !!audioPlayer?.playing
pendingSeekRef.current = null
playbackEndedRef.current = false
if (audioPlayer?.playing) await audioPlayer.pause?.()
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
videoElRef.current.pause()
}
} catch { }
}, [audioPlayer]);
} catch {}
}, [audioPlayer])
const onSeekEnd = useCallback(async () => {
try {
if (!isSeekingRef.current) return;
await waitForPendingSeek();
isSeekingRef.current = false;
if (!isSeekingRef.current) return
await waitForPendingSeek()
isSeekingRef.current = false
if (wasPlayingRef.current) {
if (audioPlayer) await audioPlayer.resume?.();
if (videoElRef.current && videoUri)
videoElRef.current.play().catch(() => { });
if (audioPlayer) await audioPlayer.resume?.()
if (videoElRef.current && videoUri) videoElRef.current.play().catch(() => {})
}
} catch { }
}, [audioPlayer, videoUri, waitForPendingSeek]);
} catch {}
}, [audioPlayer, videoUri, waitForPendingSeek])
const handleTogglePlayback = useCallback(async () => {
try {
const duration = Number(progress?.durS || 0);
const position = Number(progress?.posS || 0);
const isAtEnd = duration > 0 && duration - position < 0.35;
const isCurrentlyPlaying = !!audioPlayer?.playing;
const duration = Number(progress?.durS || 0)
const position = Number(progress?.posS || 0)
const isAtEnd = duration > 0 && duration - position < 0.35
const isCurrentlyPlaying = !!audioPlayer?.playing
if (isCurrentlyPlaying) {
playbackEndedRef.current = false;
await stopPlayback();
return;
playbackEndedRef.current = false
await stopPlayback()
return
}
if (isAtEnd) {
if (audioPlayer) await audioPlayer.seekTo?.(0);
if (audioPlayer) await audioPlayer.seekTo?.(0)
if (videoElRef.current && videoUri) {
videoElRef.current.currentTime = 0;
videoElRef.current.currentTime = 0
}
}
playbackEndedRef.current = false;
playbackEndedRef.current = false
if (songUrl && audioPlayer) {
await audioPlayer.play?.();
await audioPlayer.play?.()
}
if (videoElRef.current && videoUri) {
videoElRef.current.muted = true;
videoElRef.current.play().catch(() => { });
videoElRef.current.muted = true
videoElRef.current.play().catch(() => {})
}
} catch { }
}, [audioPlayer, songUrl, stopPlayback, progress, videoUri]);
} catch {}
}, [audioPlayer, songUrl, stopPlayback, progress, videoUri])
return (
<Page
containerStyle={{
alignItems: "none",
alignItems: 'none',
}}
backgroundColor={Palette.grayMid}
headerType="NONE"
@@ -274,7 +255,7 @@ const RecordedPlayback = ({ route }) => {
/>
<View
style={{
width: "100%",
width: '100%',
}}
>
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
@@ -282,14 +263,14 @@ const RecordedPlayback = ({ route }) => {
<Pressable
onPress={handleTogglePlayback}
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
maxWidth: '100%',
aspectRatio: 9 / 16,
borderRadius: 12,
backgroundColor: "#00000066",
overflow: "hidden",
alignSelf: "center",
position: "relative",
backgroundColor: '#00000066',
overflow: 'hidden',
alignSelf: 'center',
position: 'relative',
}}
>
<video
@@ -298,10 +279,10 @@ const RecordedPlayback = ({ route }) => {
muted
playsInline
style={{
width: "100%",
height: "100%",
objectFit: "cover",
backgroundColor: "black",
width: '100%',
height: '100%',
objectFit: 'cover',
backgroundColor: 'black',
}}
controls={false}
/>
@@ -309,18 +290,18 @@ const RecordedPlayback = ({ route }) => {
<View
pointerEvents="none"
style={{
position: "absolute",
left: "50%",
top: "50%",
position: 'absolute',
left: '50%',
top: '50%',
transform: [{ translateX: -36 }, { translateY: -36 }],
width: 72,
height: 72,
borderRadius: 36,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(10, 5, 24, 0.75)",
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(10, 5, 24, 0.75)',
borderWidth: 1,
borderColor: "#F94697",
borderColor: '#F94697',
}}
>
<Image source={icons.play} style={{ width: 26, height: 26 }} />
@@ -331,15 +312,15 @@ const RecordedPlayback = ({ route }) => {
// Placeholder quand pas de vidéo sur web
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
maxWidth: '100%',
aspectRatio: 9 / 16,
borderRadius: 12,
backgroundColor: "#00000066",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
alignSelf: "center",
backgroundColor: '#00000066',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
alignSelf: 'center',
}}
>
<Text style={{ color: Palette.white, opacity: 0.8 }}>
@@ -350,14 +331,14 @@ const RecordedPlayback = ({ route }) => {
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
alignSelf: "center",
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
maxWidth: '100%',
alignSelf: 'center',
marginTop: 6,
alignItems: "center",
alignItems: 'center',
}}
>
<View style={{ width: "100%" }}>
<View style={{ width: '100%' }}>
<Slider
value={fmtSeconds(progress.posS)} // mm:ss (seconds)
maxValue={fmtSeconds(progress.durS)} // mm:ss (seconds)
@@ -373,9 +354,9 @@ const RecordedPlayback = ({ route }) => {
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
alignSelf: "center",
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
maxWidth: '100%',
alignSelf: 'center',
gap: 12,
marginTop: 50,
}}
@@ -383,26 +364,26 @@ const RecordedPlayback = ({ route }) => {
<GradientButton
title="Je valide"
onPress={() => {
shouldPreserveBlobRef.current = true;
shouldPreserveBlobRef.current = true
navigate(Routes.PlaybackDownload, {
action: "playback",
action: 'playback',
uri: videoUri || null, // peut être null sur web
project,
});
})
}}
/>
<BorderGradientButton
title="Recommencer"
onPress={async () => {
try {
await stopPlayback();
} catch { }
navigate(Routes.RecordPlayback, { project });
await stopPlayback()
} catch {}
navigate(Routes.RecordPlayback, { project })
}}
/>
</View>
</Page>
);
};
)
}
export default RecordedPlayback;
export default RecordedPlayback
+20 -28
View File
@@ -1,54 +1,46 @@
import { View, Text, Image } from "react-native";
import React from "react";
import MusicLandHeader from "../../components/MusicLandHeader";
import Page from "../../layouts/Page";
import { background, icons, img } from "../../assets";
import { goBack, navigate } from "../../navigation/NavigationService";
import Slider from "../../components/Slider";
import GradientButton from "../../components/GradientButton";
import BorderGradientButton from "../../components/BorderGradientButton";
import { gutters } from "../../styles";
import { Routes } from "../../navigation";
import { View, Text, Image } from 'react-native'
import React from 'react'
import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page'
import { background, icons, img } from '../../assets'
import { goBack, navigate } from '../../navigation/NavigationService'
import Slider from '../../components/Slider'
import GradientButton from '../../components/GradientButton'
import BorderGradientButton from '../../components/BorderGradientButton'
import { gutters } from '../../styles'
import { Routes } from '../../navigation'
const VideoFinalize = () => {
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
<View
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}>
<View style={{ flex: 1, gap: 22 }}>
<Image
source={img.placeholder3}
style={{
width: "80%",
width: '80%',
flex: 1,
alignSelf: "center",
alignSelf: 'center',
borderRadius: 16,
}}
/>
<Slider value="0:00" maxValue="2:00" />
</View>
<View
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
>
<BorderGradientButton
icon={icons.stars}
title="Regénérer"
onPress={goBack}
/>
<View style={{ width: '80%', alignSelf: 'center', marginTop: 4, gap: 12 }}>
<BorderGradientButton icon={icons.stars} title="Regénérer" onPress={goBack} />
<GradientButton
title="Valider"
onPress={() =>
navigate(Routes.PlaybackDownload, {
action: "playback",
action: 'playback',
})
}
/>
</View>
</View>
</Page>
);
};
)
}
export default VideoFinalize;
export default VideoFinalize
+49 -50
View File
@@ -1,83 +1,82 @@
import { useIsFocused, useRoute } from "@react-navigation/native";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import Carousel from "react-native-reanimated-carousel";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import PlaybackItem from "./components/PlaybackItem";
import { useIsFocused, useRoute } from '@react-navigation/native'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { View } from 'react-native'
import Carousel from 'react-native-reanimated-carousel'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { projectsRef } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import PlaybackItem from './components/PlaybackItem'
const Playbacks = () => {
const [activeIndex, setActiveIndex] = useState(0);
const userCache = useRef(new Map());
const isFocused = useIsFocused();
const route = useRoute();
const focusProjectId =
route?.params?.projectId || route?.params?.focusId || null;
const [activeIndex, setActiveIndex] = useState(0)
const userCache = useRef(new Map())
const isFocused = useIsFocused()
const route = useRoute()
const focusProjectId = route?.params?.projectId || route?.params?.focusId || null
const { data: rawPlaybacks = [], loadMore } = useDataFromRef({
ref: projectsRef.where("playbackUrl", "!=", null),
ref: projectsRef.where('playbackUrl', '!=', null),
simpleRef: false,
listener: false,
usePagination: true,
batchSize: 6,
});
})
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1;
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId);
}, [focusProjectId, rawPlaybacks]);
if (!focusProjectId) return -1
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [focusProjectId, rawPlaybacks])
const playbacks = useMemo(() => {
if (focusIndex < 0) return rawPlaybacks;
const target = rawPlaybacks[focusIndex];
const before = rawPlaybacks.slice(0, focusIndex);
const after = rawPlaybacks.slice(focusIndex + 1);
return [target, ...after, ...before];
}, [focusIndex, rawPlaybacks]);
if (focusIndex < 0) return rawPlaybacks
const target = rawPlaybacks[focusIndex]
const before = rawPlaybacks.slice(0, focusIndex)
const after = rawPlaybacks.slice(focusIndex + 1)
return [target, ...after, ...before]
}, [focusIndex, rawPlaybacks])
const onSnap = useCallback(
(index) => {
setActiveIndex(index);
setActiveIndex(index)
if (index >= (playbacks?.length || 0) - 6) {
loadMore?.();
loadMore?.()
}
},
[playbacks?.length, loadMore],
);
[playbacks?.length, loadMore]
)
const triedLoadMoreRef = useRef(0);
const focusLoadAttemptsRef = useRef(0);
const hasAppliedFocusRef = useRef(false);
const triedLoadMoreRef = useRef(0)
const focusLoadAttemptsRef = useRef(0)
const hasAppliedFocusRef = useRef(false)
useEffect(() => {
focusLoadAttemptsRef.current = 0;
hasAppliedFocusRef.current = false;
focusLoadAttemptsRef.current = 0
hasAppliedFocusRef.current = false
if (!focusProjectId) {
setActiveIndex(0);
setActiveIndex(0)
}
}, [focusProjectId]);
}, [focusProjectId])
useEffect(() => {
if (loadMore && triedLoadMoreRef.current < 6) {
triedLoadMoreRef.current += 1;
loadMore();
triedLoadMoreRef.current += 1
loadMore()
}
}, [loadMore, playbacks]);
}, [loadMore, playbacks])
useEffect(() => {
if (!focusProjectId) return;
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
hasAppliedFocusRef.current = true;
setActiveIndex(0);
return;
hasAppliedFocusRef.current = true
setActiveIndex(0)
return
}
if (loadMore && focusLoadAttemptsRef.current < 6) {
focusLoadAttemptsRef.current += 1;
loadMore();
focusLoadAttemptsRef.current += 1
loadMore()
}
}, [focusIndex, focusProjectId, loadMore, playbacks]);
}, [focusIndex, focusProjectId, loadMore, playbacks])
return (
<View style={{ flex: 1, backgroundColor: "black" }}>
<View style={{ flex: 1, backgroundColor: 'black' }}>
<Carousel
data={playbacks}
vertical
@@ -95,7 +94,7 @@ const Playbacks = () => {
)}
/>
</View>
);
};
)
}
export default Playbacks;
export default Playbacks
+187 -221
View File
@@ -1,234 +1,217 @@
import { useIsFocused, useRoute } from "@react-navigation/native";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FlatList,
Image,
Pressable,
StyleSheet,
View,
useWindowDimensions,
} from "react-native";
import { icons } from "../../assets";
import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import PlaybackItem from "./components/PlaybackItem.web";
import { useIsFocused, useRoute } from '@react-navigation/native'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FlatList, Image, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native'
import { icons } from '../../assets'
import { projectsRef } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import PlaybackItem from './components/PlaybackItem.web'
const Playbacks = () => {
const { height: windowHeight } = useWindowDimensions();
const [activeIndex, setActiveIndex] = useState(0);
const route = useRoute();
const focusProjectId =
route?.params?.projectId || route?.params?.focusId || null;
const userCache = useRef(new Map());
const { getUserByUid } = useUser() || {};
const isFocused = useIsFocused();
const { height: windowHeight } = useWindowDimensions()
const [activeIndex, setActiveIndex] = useState(0)
const route = useRoute()
const focusProjectId = route?.params?.projectId || route?.params?.focusId || null
const userCache = useRef(new Map())
const { getUserByUid } = useUser() || {}
const isFocused = useIsFocused()
const {
data: rawPlaybacks = [],
loadMore,
hasMore,
loading,
} = useDataFromRef({
ref: projectsRef.where("playbackUrl", "!=", null),
ref: projectsRef.where('playbackUrl', '!=', null),
simpleRef: false,
listener: false,
usePagination: true,
batchSize: 6,
});
})
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1;
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId);
}, [focusProjectId, rawPlaybacks]);
if (!focusProjectId) return -1
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [focusProjectId, rawPlaybacks])
const playbacks = useMemo(() => {
if (focusIndex < 0) return rawPlaybacks;
const target = rawPlaybacks[focusIndex];
const before = rawPlaybacks.slice(0, focusIndex);
const after = rawPlaybacks.slice(focusIndex + 1);
return [target, ...after, ...before];
}, [focusIndex, rawPlaybacks]);
if (focusIndex < 0) return rawPlaybacks
const target = rawPlaybacks[focusIndex]
const before = rawPlaybacks.slice(0, focusIndex)
const after = rawPlaybacks.slice(focusIndex + 1)
return [target, ...after, ...before]
}, [focusIndex, rawPlaybacks])
const activePlayback = playbacks?.[activeIndex] || null;
const activeVideoUrl = activePlayback?.playbackUrl || null;
const activeTitle =
typeof activePlayback?.title === "string"
? activePlayback.title.trim()
: "";
const activePlayback = playbacks?.[activeIndex] || null
const activeVideoUrl = activePlayback?.playbackUrl || null
const activeTitle = typeof activePlayback?.title === 'string' ? activePlayback.title.trim() : ''
const activeCreatorName = activePlayback?.userName;
const backgroundVideoRef = useRef(null);
const lastActiveIndexRef = useRef(0);
const backgroundSeekPendingRef = useRef(false);
const listRef = useRef(null);
const activeCreatorName = activePlayback?.userName
const backgroundVideoRef = useRef(null)
const lastActiveIndexRef = useRef(0)
const backgroundSeekPendingRef = useRef(false)
const listRef = useRef(null)
useEffect(() => {
const total = playbacks?.length || 0;
const total = playbacks?.length || 0
if (total <= 0) {
if (activeIndex !== 0) {
lastActiveIndexRef.current = 0;
setActiveIndex(0);
lastActiveIndexRef.current = 0
setActiveIndex(0)
}
return;
return
}
const maxIndex = total - 1;
const maxIndex = total - 1
if (activeIndex > maxIndex) {
lastActiveIndexRef.current = maxIndex;
setActiveIndex(maxIndex);
lastActiveIndexRef.current = maxIndex
setActiveIndex(maxIndex)
}
}, [playbacks?.length, activeIndex]);
}, [playbacks?.length, activeIndex])
const handleActiveIndexChange = useCallback(
(rawIndex) => {
if (Number.isNaN(rawIndex)) return;
const total = playbacks?.length || 0;
const maxIndex = Math.max(0, total - 1);
const clamped = Math.max(0, Math.min(rawIndex, maxIndex));
if (clamped === lastActiveIndexRef.current) return;
lastActiveIndexRef.current = clamped;
setActiveIndex((prev) => (prev === clamped ? prev : clamped));
if (Number.isNaN(rawIndex)) return
const total = playbacks?.length || 0
const maxIndex = Math.max(0, total - 1)
const clamped = Math.max(0, Math.min(rawIndex, maxIndex))
if (clamped === lastActiveIndexRef.current) return
lastActiveIndexRef.current = clamped
setActiveIndex((prev) => (prev === clamped ? prev : clamped))
if (total > 0 && clamped >= Math.max(0, total - 6)) {
loadMore?.();
loadMore?.()
}
},
[playbacks?.length, loadMore],
);
[playbacks?.length, loadMore]
)
const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => {
const bg = backgroundVideoRef.current;
if (!bg) return;
const bg = backgroundVideoRef.current
if (!bg) return
if (typeof currentTime === "number" && Number.isFinite(currentTime)) {
if (typeof currentTime === 'number' && Number.isFinite(currentTime)) {
const applyTime = () => {
backgroundSeekPendingRef.current = false;
const diff = Math.abs((bg.currentTime || 0) - currentTime);
backgroundSeekPendingRef.current = false
const diff = Math.abs((bg.currentTime || 0) - currentTime)
if (diff > 0.25) {
try {
bg.currentTime = currentTime;
bg.currentTime = currentTime
} catch (_e) {}
}
};
}
if (bg.readyState >= 1) applyTime();
if (bg.readyState >= 1) applyTime()
else if (!backgroundSeekPendingRef.current) {
backgroundSeekPendingRef.current = true;
const handler = () => applyTime();
bg.addEventListener("loadeddata", handler, { once: true });
backgroundSeekPendingRef.current = true
const handler = () => applyTime()
bg.addEventListener('loadeddata', handler, { once: true })
}
}
if (isPlaying === true) {
if (bg.paused) {
bg.play().catch(() => {});
bg.play().catch(() => {})
}
} else if (isPlaying === false) {
try {
if (!bg.paused) bg.pause();
if (!bg.paused) bg.pause()
} catch (_e) {}
}
}, []);
}, [])
useEffect(() => {
const bg = backgroundVideoRef.current;
if (!bg) return;
const bg = backgroundVideoRef.current
if (!bg) return
if (!activeVideoUrl) {
backgroundSeekPendingRef.current = false;
backgroundSeekPendingRef.current = false
try {
bg.pause();
bg.pause()
} catch (_e) {}
bg.removeAttribute?.("src");
bg.load?.();
return;
bg.removeAttribute?.('src')
bg.load?.()
return
}
backgroundSeekPendingRef.current = false;
backgroundSeekPendingRef.current = false
const setSrcIfNeeded = () => {
const attrSrc = bg.getAttribute("src");
const attrSrc = bg.getAttribute('src')
if (attrSrc !== activeVideoUrl) {
bg.setAttribute("src", activeVideoUrl);
bg.setAttribute('src', activeVideoUrl)
try {
bg.load();
bg.load()
} catch (_e) {}
}
};
}
const startPlayback = () => {
bg.play().catch(() => {});
};
bg.play().catch(() => {})
}
setSrcIfNeeded();
setSrcIfNeeded()
if (bg.readyState >= 2) {
startPlayback();
return;
startPlayback()
return
}
const handleLoaded = () => {
startPlayback();
};
bg.addEventListener("loadeddata", handleLoaded, { once: true });
startPlayback()
}
bg.addEventListener('loadeddata', handleLoaded, { once: true })
return () => {
bg.removeEventListener("loadeddata", handleLoaded);
};
}, [activeVideoUrl]);
bg.removeEventListener('loadeddata', handleLoaded)
}
}, [activeVideoUrl])
const triedLoadMoreRef = useRef(0);
const hasAppliedFocusRef = useRef(false);
const focusLoadAttemptsRef = useRef(0);
const triedLoadMoreRef = useRef(0)
const hasAppliedFocusRef = useRef(false)
const focusLoadAttemptsRef = useRef(0)
useEffect(() => {
hasAppliedFocusRef.current = false;
focusLoadAttemptsRef.current = 0;
}, [focusProjectId]);
hasAppliedFocusRef.current = false
focusLoadAttemptsRef.current = 0
}, [focusProjectId])
useEffect(() => {
if (!focusProjectId) return;
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
hasAppliedFocusRef.current = true;
lastActiveIndexRef.current = 0;
setActiveIndex(0);
hasAppliedFocusRef.current = true
lastActiveIndexRef.current = 0
setActiveIndex(0)
requestAnimationFrame(() => {
try {
listRef.current?.scrollToOffset?.({ offset: 0, animated: false });
listRef.current?.scrollToOffset?.({ offset: 0, animated: false })
} catch (_e) {}
});
return;
})
return
}
if (loadMore && focusLoadAttemptsRef.current < 6) {
focusLoadAttemptsRef.current += 1;
loadMore();
focusLoadAttemptsRef.current += 1
loadMore()
}
}, [focusIndex, focusProjectId, loadMore, playbacks]);
}, [focusIndex, focusProjectId, loadMore, playbacks])
// === FlatList (one real page per item) ===
// keep active index in sync with scroll
const onMomentumScrollEnd = useCallback(
(e) => {
try {
const y = e?.nativeEvent?.contentOffset?.y || 0;
const idx = Math.round(y / windowHeight);
handleActiveIndexChange(idx);
const y = e?.nativeEvent?.contentOffset?.y || 0
const idx = Math.round(y / windowHeight)
handleActiveIndexChange(idx)
} catch (_e) {}
},
[windowHeight, handleActiveIndexChange],
);
[windowHeight, handleActiveIndexChange]
)
const onScroll = useCallback(
(e) => {
try {
const y = e?.nativeEvent?.contentOffset?.y || 0;
const idx = Math.round(y / windowHeight);
handleActiveIndexChange(idx);
const y = e?.nativeEvent?.contentOffset?.y || 0
const idx = Math.round(y / windowHeight)
handleActiveIndexChange(idx)
} catch (_e) {}
},
[windowHeight, handleActiveIndexChange],
);
[windowHeight, handleActiveIndexChange]
)
const getItemLayout = useCallback(
(_data, index) => ({
@@ -236,90 +219,80 @@ const Playbacks = () => {
offset: windowHeight * index,
index,
}),
[windowHeight],
);
[windowHeight]
)
const total = playbacks.length;
const total = playbacks.length
const indicatorItems = useMemo(() => {
if (total <= 0) return [];
const maxVisible = 7;
let start = 0;
let end = total - 1;
if (total <= 0) return []
const maxVisible = 7
let start = 0
let end = total - 1
if (total > maxVisible) {
const half = Math.floor(maxVisible / 2);
start = activeIndex - half;
end = start + maxVisible - 1;
const half = Math.floor(maxVisible / 2)
start = activeIndex - half
end = start + maxVisible - 1
if (start < 0) {
end += -start;
start = 0;
end += -start
start = 0
}
if (end >= total) {
const overshoot = end - (total - 1);
start = Math.max(0, start - overshoot);
end = total - 1;
const overshoot = end - (total - 1)
start = Math.max(0, start - overshoot)
end = total - 1
}
}
const items = [];
const items = []
for (let i = start; i <= end; i += 1) {
items.push({
key: `indicator-${i}`,
isActive: i === activeIndex,
isPast: i < activeIndex,
});
})
}
return items;
}, [total, activeIndex]);
return items
}, [total, activeIndex])
const canScrollUp = total > 0 && activeIndex > 0;
const atLastLoaded = total > 0 && activeIndex >= total - 1;
const canScrollDown = total > 0 && (!atLastLoaded || hasMore);
const showIndicators = total > 0;
const canScrollUp = total > 0 && activeIndex > 0
const atLastLoaded = total > 0 && activeIndex >= total - 1
const canScrollDown = total > 0 && (!atLastLoaded || hasMore)
const showIndicators = total > 0
const indicatorLabel = useMemo(() => {
if (total <= 0) return null;
if (hasMore || loading) return `${activeIndex + 1}+`;
return `${activeIndex + 1}/${total}`;
}, [total, activeIndex, hasMore, loading]);
if (total <= 0) return null
if (hasMore || loading) return `${activeIndex + 1}+`
return `${activeIndex + 1}/${total}`
}, [total, activeIndex, hasMore, loading])
const scrollToPlayback = useCallback(
(direction) => {
if (!direction || !listRef.current) return;
const totalItems = playbacks.length;
if (totalItems <= 0) return;
const targetIndex = Math.max(
0,
Math.min(activeIndex + direction, totalItems - 1),
);
if (!direction || !listRef.current) return
const totalItems = playbacks.length
if (totalItems <= 0) return
const targetIndex = Math.max(0, Math.min(activeIndex + direction, totalItems - 1))
if (targetIndex === activeIndex) {
if (direction > 0 && hasMore) {
loadMore?.();
loadMore?.()
}
return;
return
}
const targetOffset = targetIndex * windowHeight;
const targetOffset = targetIndex * windowHeight
try {
listRef.current.scrollToOffset({
offset: targetOffset,
animated: true,
});
})
} catch (_e) {
try {
listRef.current.scrollToIndex({
index: targetIndex,
animated: true,
});
})
} catch (__e) {}
}
handleActiveIndexChange(targetIndex);
handleActiveIndexChange(targetIndex)
},
[
activeIndex,
handleActiveIndexChange,
hasMore,
loadMore,
playbacks.length,
windowHeight,
],
);
[activeIndex, handleActiveIndexChange, hasMore, loadMore, playbacks.length, windowHeight]
)
return (
<View style={styles.screen}>
@@ -335,13 +308,13 @@ const Playbacks = () => {
playsInline
preload="auto"
style={{
position: "absolute",
position: 'absolute',
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
filter: "blur(28px) saturate(120%) brightness(55%)",
transform: "scale(1.1)",
width: '100%',
height: '100%',
objectFit: 'cover',
filter: 'blur(28px) saturate(120%) brightness(55%)',
transform: 'scale(1.1)',
}}
/>
<View style={styles.backgroundOverlay} />
@@ -352,7 +325,7 @@ const Playbacks = () => {
data={playbacks}
keyExtractor={(item, index) => String(item?.id || index)}
renderItem={({ item, index }) => (
<View style={{ width: "100%", height: windowHeight }}>
<View style={{ width: '100%', height: windowHeight }}>
<PlaybackItem
item={item}
userCache={userCache}
@@ -398,16 +371,12 @@ const Playbacks = () => {
styles.indicatorDot,
item.isActive && styles.indicatorDotActive,
item.isPast && styles.indicatorDotPast,
index < indicatorItems.length - 1
? styles.indicatorDotSpacing
: null,
index < indicatorItems.length - 1 ? styles.indicatorDotSpacing : null,
]}
/>
))}
{canScrollDown && hasMore && (
<View
style={[styles.indicatorMoreDot, styles.indicatorDotSpacing]}
/>
<View style={[styles.indicatorMoreDot, styles.indicatorDotSpacing]} />
)}
</View>
{/* {indicatorLabel ? (
@@ -422,53 +391,50 @@ const Playbacks = () => {
<Image
source={icons.chevronDown}
resizeMode="contain"
style={[
styles.indicatorArrow,
!canScrollDown && styles.indicatorArrowDisabled,
]}
style={[styles.indicatorArrow, !canScrollDown && styles.indicatorArrowDisabled]}
/>
</Pressable>
</View>
)}
</View>
// </Page>
);
};
)
}
export default Playbacks;
export default Playbacks
const styles = StyleSheet.create({
screen: {
flex: 1,
},
backgroundVideoWrapper: {
height: "100%",
width: "100%",
position: "absolute",
height: '100%',
width: '100%',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
overflow: "hidden",
overflow: 'hidden',
zIndex: -1,
backgroundColor: "#060606",
backgroundColor: '#060606',
},
backgroundOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(2, 2, 2, 0.35)",
backgroundColor: 'rgba(2, 2, 2, 0.35)',
},
topOverlayContainer: {
position: "absolute",
position: 'absolute',
top: 32,
left: 24,
right: 24,
zIndex: 10,
},
topOverlayContent: {
alignSelf: "flex-start",
alignSelf: 'flex-start',
paddingVertical: 12,
paddingHorizontal: 18,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.45)",
backgroundColor: 'rgba(0,0,0,0.45)',
gap: 4,
},
topOverlayTitle: {
@@ -483,12 +449,12 @@ const styles = StyleSheet.create({
opacity: 0.9,
},
indicatorContainer: {
position: "absolute",
position: 'absolute',
left: 20,
top: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
indicatorArrow: {
@@ -502,22 +468,22 @@ const styles = StyleSheet.create({
paddingHorizontal: 8,
},
indicatorArrowUp: {
transform: [{ rotate: "180deg" }],
transform: [{ rotate: '180deg' }],
},
indicatorArrowDisabled: {
opacity: 0.25,
},
indicatorDotsWrapper: {
alignItems: "center",
alignItems: 'center',
},
indicatorDot: {
width: 6,
height: 12,
borderRadius: 3,
backgroundColor: "rgba(255,255,255,0.3)",
backgroundColor: 'rgba(255,255,255,0.3)',
},
indicatorDotPast: {
backgroundColor: "rgba(255,255,255,0.55)",
backgroundColor: 'rgba(255,255,255,0.55)',
},
indicatorDotActive: {
backgroundColor: Palette.white,
@@ -537,6 +503,6 @@ const styles = StyleSheet.create({
color: Palette.white,
fontSize: 12,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
});
})
@@ -1,13 +1,7 @@
import { BlurView } from "expo-blur";
import moment from "moment";
import "moment/locale/fr";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { BlurView } from 'expo-blur'
import moment from 'moment'
import 'moment/locale/fr'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
Image,
Platform,
@@ -17,32 +11,28 @@ import {
Text,
TextInput,
View,
} from "react-native";
import { icons } from "../../../assets";
import ProfilePicture from "../../../components/ProfilePicture";
import {
increment,
projectsRef,
serverTimestamp,
} from "../../../config/firebase";
import useDataFromRef from "../../../hooks/useDataFromRef";
import { useUser } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
import { getArtistDisplayName } from "../../../utils/artistName";
import { ensureAuthenticated } from "../../../utils/authRedirect";
} from 'react-native'
import { icons } from '../../../assets'
import ProfilePicture from '../../../components/ProfilePicture'
import { increment, projectsRef, serverTimestamp } from '../../../config/firebase'
import useDataFromRef from '../../../hooks/useDataFromRef'
import { useUser } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { size } from '../../../styles/Style'
import { getArtistDisplayName } from '../../../utils/artistName'
import { ensureAuthenticated } from '../../../utils/authRedirect'
moment.locale("fr");
moment.locale('fr')
const formatRelativeTime = (date) => {
try {
const d = date instanceof Date ? date : date?.toDate?.() || null;
return d ? moment(d).fromNow() : "";
const d = date instanceof Date ? date : date?.toDate?.() || null
return d ? moment(d).fromNow() : ''
} catch (_e) {
return "";
return ''
}
};
}
const CommentsPanel = ({
projectId,
@@ -52,37 +42,31 @@ const CommentsPanel = ({
inputRef,
panelHeight,
}) => {
const { currentUID, currentUserData } = useUser() || {};
const { currentUID, currentUserData } = useUser() || {}
const currentUserDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, ""),
() => getArtistDisplayName(currentUserData, ''),
[currentUserData]
);
const [text, setText] = useState("");
const scrollRef = useRef(null);
const requireAuth = useCallback(
() => ensureAuthenticated(currentUID),
[currentUID]
);
)
const [text, setText] = useState('')
const scrollRef = useRef(null)
const requireAuth = useCallback(() => ensureAuthenticated(currentUID), [currentUID])
useEffect(() => {
setText("");
setText('')
try {
scrollRef.current?.scrollTo({ y: 0, animated: false });
scrollRef.current?.scrollTo({ y: 0, animated: false })
} catch (_e) {}
}, [projectId]);
}, [projectId])
const commentsRef = useMemo(() => {
try {
return projectId
? projectsRef
.doc(projectId)
.collection("comments")
.orderBy("createdAt", "desc")
: null;
? projectsRef.doc(projectId).collection('comments').orderBy('createdAt', 'desc')
: null
} catch (_e) {
return null;
return null
}
}, [projectId]);
}, [projectId])
const {
data: comments = [],
@@ -96,85 +80,65 @@ const CommentsPanel = ({
refreshArray: [projectId],
usePagination: true,
batchSize: 20,
});
})
const handleScroll = useCallback(
({ nativeEvent }) => {
try {
const { layoutMeasurement, contentOffset, contentSize } =
nativeEvent || {};
if (
layoutMeasurement?.height + contentOffset?.y >=
(contentSize?.height || 0) - 120
) {
loadMore?.();
const { layoutMeasurement, contentOffset, contentSize } = nativeEvent || {}
if (layoutMeasurement?.height + contentOffset?.y >= (contentSize?.height || 0) - 120) {
loadMore?.()
}
} catch (_e) {}
},
[loadMore]
);
)
const onSend = useCallback(async () => {
const value = (text || "").trim();
if (!value || !projectId) return;
if (!requireAuth()) return;
const value = (text || '').trim()
if (!value || !projectId) return
if (!requireAuth()) return
try {
setText("");
setText('')
const docRef = await projectsRef
.doc(projectId)
.collection("comments")
.collection('comments')
.add({
userId: currentUID,
userName: currentUserDisplayName,
profilePicture: currentUserData?.profilePictureURL || "",
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: serverTimestamp(),
});
})
try {
await projectsRef
.doc(projectId)
.set({ commentsCount: increment(1) }, { merge: true });
await projectsRef.doc(projectId).set({ commentsCount: increment(1) }, { merge: true })
} catch (_e) {}
const optimistic = {
id: docRef?.id || Math.random().toString(36).slice(2),
userId: currentUID,
userName: currentUserDisplayName,
profilePicture: currentUserData?.profilePictureURL || "",
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: new Date(),
};
setComments((prev = []) => [
optimistic,
...prev.filter((x) => x?.id !== optimistic.id),
]);
onCommentAdded?.();
}
setComments((prev = []) => [optimistic, ...prev.filter((x) => x?.id !== optimistic.id)])
onCommentAdded?.()
requestAnimationFrame(() => {
try {
scrollRef.current?.scrollTo({ y: 0, animated: true });
scrollRef.current?.scrollTo({ y: 0, animated: true })
} catch (_e) {}
});
})
} catch (_e) {}
}, [
currentUID,
currentUserData,
onCommentAdded,
projectId,
requireAuth,
setComments,
text,
]);
}, [currentUID, currentUserData, onCommentAdded, projectId, requireAuth, setComments, text])
const canComment = !!currentUID;
const descriptionText = (description || "").trim();
const canComment = !!currentUID
const descriptionText = (description || '').trim()
return (
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
tint="dark"
style={[
styles.commentsWrapper,
panelHeight ? { height: panelHeight } : null,
]}
style={[styles.commentsWrapper, panelHeight ? { height: panelHeight } : null]}
>
<View style={styles.commentsInner}>
<ScrollView
@@ -190,21 +154,15 @@ const CommentsPanel = ({
<Text style={styles.sectionTitle}>Description</Text>
<View style={styles.descriptionCard}>
<Text style={styles.descriptionText}>
{descriptionText || "Description chanson..."}
{descriptionText || 'Description chanson...'}
</Text>
</View>
<Text style={[styles.sectionTitle, styles.sectionSpacing]}>
{`Commentaires${commentsCount ? ` (${commentsCount})` : ""}`}
{`Commentaires${commentsCount ? ` (${commentsCount})` : ''}`}
</Text>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c, index) => (
<View
key={c.id}
style={[
styles.commentRow,
index > 0 && styles.commentRowSpacing,
]}
>
<View key={c.id} style={[styles.commentRow, index > 0 && styles.commentRowSpacing]}>
<ProfilePicture
uri={c?.profilePicture || null}
size={48}
@@ -213,20 +171,16 @@ const CommentsPanel = ({
<View style={styles.commentBubble}>
<View style={styles.commentHeader}>
<Text style={styles.commentAuthor}>
{c?.userId === currentUID ? "vous" : c?.userName || ""}
</Text>
<Text style={styles.commentMeta}>
• {formatRelativeTime(c?.createdAt)}
{c?.userId === currentUID ? 'vous' : c?.userName || ''}
</Text>
<Text style={styles.commentMeta}>• {formatRelativeTime(c?.createdAt)}</Text>
</View>
<Text style={styles.commentBody}>{c?.text}</Text>
</View>
</View>
))
) : (
<Text style={styles.emptyState}>
Aucun commentaire pour le moment
</Text>
<Text style={styles.emptyState}>Aucun commentaire pour le moment</Text>
)}
</ScrollView>
<View style={styles.commentInputContainer}>
@@ -235,28 +189,24 @@ const CommentsPanel = ({
value={text}
onChangeText={(value) => {
if (!requireAuth()) {
return;
return
}
setText(value);
setText(value)
}}
placeholder={
canComment
? "Commente"
: "Connecte-toi pour laisser un commentaire"
}
placeholder={canComment ? 'Commente' : 'Connecte-toi pour laisser un commentaire'}
placeholderTextColor="#FFFFFF90"
editable
onFocus={() => {
if (!requireAuth()) {
try {
inputRef.current?.blur?.();
inputRef.current?.blur?.()
} catch (_e) {}
}
}}
onPressIn={() => {
if (!requireAuth()) {
try {
inputRef.current?.blur?.();
inputRef.current?.blur?.()
} catch (_e) {}
}
}}
@@ -271,32 +221,28 @@ const CommentsPanel = ({
pressed && text.trim() && styles.sendButtonPressed,
]}
>
<Image
source={icons.send}
style={styles.sendIcon}
resizeMode="contain"
/>
<Image source={icons.send} style={styles.sendIcon} resizeMode="contain" />
</Pressable>
</View>
</View>
</BlurView>
);
};
)
}
export default CommentsPanel;
export default CommentsPanel
const styles = StyleSheet.create({
commentsWrapper: {
flex: 1,
borderRadius: 28,
overflow: "hidden",
overflow: 'hidden',
padding: 24,
backgroundColor: Palette.glass,
minHeight: 0,
},
commentsInner: {
flex: 1,
justifyContent: "space-between",
justifyContent: 'space-between',
},
commentsScroll: {
flex: 1,
@@ -316,7 +262,7 @@ const styles = StyleSheet.create({
},
descriptionCard: {
borderRadius: 18,
backgroundColor: "#FFFFFF14",
backgroundColor: '#FFFFFF14',
paddingHorizontal: 18,
paddingVertical: 14,
},
@@ -327,8 +273,8 @@ const styles = StyleSheet.create({
lineHeight: 20,
},
commentRow: {
flexDirection: "row",
alignItems: "flex-start",
flexDirection: 'row',
alignItems: 'flex-start',
},
commentRowSpacing: {
marginTop: 16,
@@ -338,14 +284,14 @@ const styles = StyleSheet.create({
},
commentBubble: {
flex: 1,
backgroundColor: "#FFFFFF18",
backgroundColor: '#FFFFFF18',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 12,
},
commentHeader: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
commentAuthor: {
@@ -355,7 +301,7 @@ const styles = StyleSheet.create({
},
commentMeta: {
fontSize: 12,
color: "#FFFFFFA0",
color: '#FFFFFFA0',
fontFamily: FONT_FAMILY.InterRegular,
marginLeft: 8,
},
@@ -367,14 +313,14 @@ const styles = StyleSheet.create({
},
emptyState: {
fontSize: 14,
color: "#FFFFFFBB",
color: '#FFFFFFBB',
fontFamily: FONT_FAMILY.InterRegular,
},
commentInputContainer: {
marginTop: 20,
flexDirection: "row",
alignItems: "center",
backgroundColor: "#FFFFFF14",
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF14',
borderRadius: 18,
paddingHorizontal: 18,
paddingVertical: 10,
@@ -390,19 +336,19 @@ const styles = StyleSheet.create({
sendButton: {
...size({ size: 38 }),
borderRadius: 19,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Palette.primary,
},
sendButtonPressed: {
transform: [{ scale: 0.95 }],
},
sendButtonDisabled: {
backgroundColor: "#FFFFFF33",
backgroundColor: '#FFFFFF33',
},
sendIcon: {
width: 22,
height: 22,
tintColor: Palette.white,
},
});
})
@@ -1,12 +1,12 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { Feather } from "@expo/vector-icons";
import ProfilePicture from "../../../components/ProfilePicture";
import { icons } from "../../../assets";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import ProfilePicture from '../../../components/ProfilePicture'
import { icons } from '../../../assets'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { size } from '../../../styles/Style'
const PlaybackActions = ({
owner,
@@ -30,22 +30,20 @@ const PlaybackActions = ({
<ProfilePicture
uri={owner?.profilePictureURL || null}
size={45}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
</Pressable>
{owner?.id && owner.id !== currentUID && (
<Pressable disabled={isFollowActionPending} onPress={onToggleFollow}>
<BlurView
tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={[
styles.followButton,
{ backgroundColor: isFollowing ? "#FFFFFF1A" : undefined },
{ backgroundColor: isFollowing ? '#FFFFFF1A' : undefined },
]}
>
<Text style={styles.followText}>
{isFollowing ? "Suivi(e)" : "Suivre"}
</Text>
<Text style={styles.followText}>{isFollowing ? 'Suivi(e)' : 'Suivre'}</Text>
</BlurView>
</Pressable>
)}
@@ -70,38 +68,38 @@ const PlaybackActions = ({
<Text style={styles.countText}>Signaler</Text>
</Pressable>
</View>
);
};
)
}
const styles = StyleSheet.create({
actionsColumn: {
alignSelf: "flex-end",
alignItems: "center",
alignSelf: 'flex-end',
alignItems: 'center',
gap: 20,
paddingHorizontal: 13,
},
profileWrapper: { gap: 6, alignItems: "center" },
profileWrapper: { gap: 6, alignItems: 'center' },
followButton: {
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: 12,
borderWidth: 1,
borderColor: Palette.white,
overflow: "hidden",
overflow: 'hidden',
},
followText: {
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
},
centered: { alignItems: "center" },
centered: { alignItems: 'center' },
countText: {
color: Palette.white,
fontSize: 11,
marginTop: 4,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
});
})
export default PlaybackActions;
export default PlaybackActions
@@ -1,147 +1,131 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { View, StyleSheet } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { SheetManager } from "react-native-actions-sheet";
import { useUser } from "../../../providers/UserDataProvider";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { openComments } from "../../../components/bottomsheets/CommentsBottomSheet";
import {
createPlaybackSharePayload,
openShareSheet,
} from "../../../utils/shareSheet";
import {
getProjectLikes,
LIKE_TARGET,
toggleProjectLike,
} from "../../../utils/likes";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import PlaybackVideo from "./PlaybackVideo";
import PlaybackActions from "./PlaybackActions";
import PlaybackLyricsCard from "./PlaybackLyricsCard";
import usePlaybackOwner from "./usePlaybackOwner";
import usePlaybackPlayer from "./usePlaybackPlayer";
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { View, StyleSheet } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { SheetManager } from 'react-native-actions-sheet'
import { useUser } from '../../../providers/UserDataProvider'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { openComments } from '../../../components/bottomsheets/CommentsBottomSheet'
import { createPlaybackSharePayload, openShareSheet } from '../../../utils/shareSheet'
import { getProjectLikes, LIKE_TARGET, toggleProjectLike } from '../../../utils/likes'
import { ensureAuthenticated } from '../../../utils/authRedirect'
import PlaybackVideo from './PlaybackVideo'
import PlaybackActions from './PlaybackActions'
import PlaybackLyricsCard from './PlaybackLyricsCard'
import usePlaybackOwner from './usePlaybackOwner'
import usePlaybackPlayer from './usePlaybackPlayer'
const PlaybackItem = ({ item, isActive, userCache }) => {
const { currentUID, followUser, unfollowUser, getUserByUid } = useUser() || {};
const { owner, isFollowing, isFollowActionPending, toggleFollow } =
usePlaybackOwner({
item,
userCache,
getUserByUid,
currentUID,
followUser,
unfollowUser,
});
const { currentUID, followUser, unfollowUser, getUserByUid } = useUser() || {}
const { owner, isFollowing, isFollowActionPending, toggleFollow } = usePlaybackOwner({
item,
userCache,
getUserByUid,
currentUID,
followUser,
unfollowUser,
})
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
const [isLiked, setIsLiked] = useState(
currentUID ? initialLikedBy.includes(currentUID) : false
);
const [likesCount, setLikesCount] = useState(initialLikedBy.length);
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
const [isLiked, setIsLiked] = useState(currentUID ? initialLikedBy.includes(currentUID) : false)
const [likesCount, setLikesCount] = useState(initialLikedBy.length)
useEffect(() => {
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
setLikesCount(lb.length);
setIsLiked(currentUID ? lb.includes(currentUID) : false);
}, [item?.likes?.playback, currentUID]);
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
setLikesCount(lb.length)
setIsLiked(currentUID ? lb.includes(currentUID) : false)
}, [item?.likes?.playback, currentUID])
const commentsCount = useMemo(
() => Number(item?.commentsCount || 0),
[item?.commentsCount]
);
const commentsCount = useMemo(() => Number(item?.commentsCount || 0), [item?.commentsCount])
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
const projectTitle = typeof item?.title === 'string' ? item.title.trim() : ''
const creatorName = useMemo(() => {
if (owner?.displayName) return owner.displayName;
if (owner?.artistName) return owner.artistName;
if (owner?.userName) return owner.userName;
if (typeof item?.userName === "string") return item.userName;
return "";
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
if (owner?.displayName) return owner.displayName
if (owner?.artistName) return owner.artistName
if (owner?.userName) return owner.userName
if (typeof item?.userName === 'string') return item.userName
return ''
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName])
const { videoUrl, videoPlayer, currentTimeS } = usePlaybackPlayer({
item,
isActive,
});
})
const alignedWords = useMemo(() => {
const idx = Number(item?.songIndex) || 0;
const ts = item?.musicTimestamps?.[idx];
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
const idx = Number(item?.songIndex) || 0
const ts = item?.musicTimestamps?.[idx]
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
return arr.map((w) => ({
word: String(w?.word ?? ""),
word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
}));
}, [item?.musicTimestamps, item?.songIndex]);
}))
}, [item?.musicTimestamps, item?.songIndex])
const sharePayload = useMemo(() => {
if (!item?.id) return null;
if (!item?.id) return null
return createPlaybackSharePayload({
projectId: item.id,
title: projectTitle || undefined,
artist: creatorName || undefined,
playbackUrl: videoUrl || undefined,
});
}, [creatorName, item?.id, projectTitle, videoUrl]);
})
}, [creatorName, item?.id, projectTitle, videoUrl])
const handleShare = useCallback(() => {
if (sharePayload) {
openShareSheet(sharePayload);
openShareSheet(sharePayload)
}
}, [sharePayload]);
}, [sharePayload])
const handleReport = useCallback(() => {
if (!item?.id) return;
SheetManager.show("Report", {
if (!item?.id) return
SheetManager.show('Report', {
payload: {
targetType: "playback",
targetType: 'playback',
projectId: item?.id || null,
playbackId: item?.id || null,
title: item?.title || "",
title: item?.title || '',
ownerId: item?.userId || null,
},
});
}, [item?.id, item?.title, item?.userId]);
})
}, [item?.id, item?.title, item?.userId])
const handleLike = useCallback(async () => {
try {
if (!item?.id) return;
if (!item?.id) return
if (!ensureAuthenticated(currentUID)) {
return;
return
}
const nextLiked = !isLiked;
setIsLiked(nextLiked);
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
const nextLiked = !isLiked
setIsLiked(nextLiked)
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)))
await toggleProjectLike({
projectId: item.id,
currentUID,
target: LIKE_TARGET.PLAYBACK,
next: nextLiked,
});
})
} catch (e) {
setIsLiked((v) => !v);
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)));
setIsLiked((v) => !v)
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)))
}
}, [currentUID, isLiked, item?.id]);
}, [currentUID, isLiked, item?.id])
const handleCommentPress = useCallback(() => {
if (item?.id) openComments(item.id);
}, [item?.id]);
if (item?.id) openComments(item.id)
}, [item?.id])
const onOpenProfile = useCallback(() => {
navigate(Routes.SingerProfile, { userId: item?.userId });
}, [item?.userId]);
navigate(Routes.SingerProfile, { userId: item?.userId })
}, [item?.userId])
return (
<View style={styles.container}>
<PlaybackVideo
videoUrl={videoUrl}
videoPlayer={videoPlayer}
/>
<PlaybackVideo videoUrl={videoUrl} videoPlayer={videoPlayer} />
<View style={styles.overlay}>
<PlaybackActions
owner={owner}
@@ -165,21 +149,21 @@ const PlaybackItem = ({ item, isActive, userCache }) => {
/>
</View>
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
height: responsiveHeight(100),
position: "relative",
backgroundColor: "black",
position: 'relative',
backgroundColor: 'black',
},
overlay: {
position: "absolute",
width: "100%",
position: 'absolute',
width: '100%',
bottom: 130,
gap: 18,
},
});
})
export default PlaybackItem;
export default PlaybackItem
@@ -1,365 +1,330 @@
import { BlurView } from "expo-blur";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Image,
Pressable,
StyleSheet,
Text,
View,
useWindowDimensions,
} from "react-native";
import { icons, img } from "../../../assets";
import KaraokeLyrics from "../../../components/KaraokeLyrics";
import { usersRef } from "../../../config/firebase";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { useUser } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
import CommentsPanel from "./CommentsPanel.web";
import {
getProjectLikes,
LIKE_TARGET,
toggleProjectLike,
} from "../../../utils/likes";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import {
createPlaybackSharePayload,
openShareSheet,
} from "../../../utils/shareSheet";
import { SheetManager } from "react-native-actions-sheet";
import { Feather } from "@expo/vector-icons";
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native'
import { icons, img } from '../../../assets'
import KaraokeLyrics from '../../../components/KaraokeLyrics'
import { usersRef } from '../../../config/firebase'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { useUser } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { size } from '../../../styles/Style'
import CommentsPanel from './CommentsPanel.web'
import { getProjectLikes, LIKE_TARGET, toggleProjectLike } from '../../../utils/likes'
import { ensureAuthenticated } from '../../../utils/authRedirect'
import { createPlaybackSharePayload, openShareSheet } from '../../../utils/shareSheet'
import { SheetManager } from 'react-native-actions-sheet'
import { Feather } from '@expo/vector-icons'
// Debug logging toggle for web playback
const DEBUG_PLAYBACK_WEB = true;
const DEBUG_PLAYBACK_WEB = true
// NOTE: Web-only implementation that uses a native <video> element instead of expo-video
// - No Platform checks
// - Audio now comes directly from the MP4 playbackUrl
const PlaybackItem = ({
item,
isActive,
userCache,
getUserByUid,
onBackgroundSync,
}) => {
const { width: viewportWidth, height: viewportHeight } =
useWindowDimensions();
const PlaybackItem = ({ item, isActive, userCache, getUserByUid, onBackgroundSync }) => {
const { width: viewportWidth, height: viewportHeight } = useWindowDimensions()
const [layoutSize, setLayoutSize] = useState({
width: viewportWidth,
height: viewportHeight,
});
const [openComments] = useState(true);
const commentInputRef = useRef(null);
const { currentUID, followUser, unfollowUser } = useUser() || {};
})
const [openComments] = useState(true)
const commentInputRef = useRef(null)
const { currentUID, followUser, unfollowUser } = useUser() || {}
const videoUrl = item?.playbackUrl || null;
const videoRef = useRef(null); // HTMLVideoElement
const wasActiveRef = useRef(false);
const startedRef = useRef(false);
const videoUrl = item?.playbackUrl || null
const videoRef = useRef(null) // HTMLVideoElement
const wasActiveRef = useRef(false)
const startedRef = useRef(false)
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
const [isLiked, setIsLiked] = useState(
currentUID ? initialLikedBy.includes(currentUID) : false
);
const [likesCount, setLikesCount] = useState(initialLikedBy.length);
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
const [isLiked, setIsLiked] = useState(currentUID ? initialLikedBy.includes(currentUID) : false)
const [likesCount, setLikesCount] = useState(initialLikedBy.length)
const computedCommentsCount = useMemo(
() => Number(item?.commentsCount || 0),
[item?.commentsCount]
);
const [commentsCount, setCommentsCount] = useState(computedCommentsCount);
)
const [commentsCount, setCommentsCount] = useState(computedCommentsCount)
useEffect(() => {
setCommentsCount(computedCommentsCount);
}, [computedCommentsCount]);
setCommentsCount(computedCommentsCount)
}, [computedCommentsCount])
const [owner, setOwner] = useState(
item?.userId && userCache?.current?.get(item.userId)
? userCache.current.get(item.userId)
: null
);
item?.userId && userCache?.current?.get(item.userId) ? userCache.current.get(item.userId) : null
)
useEffect(() => {
let cancelled = false;
let cancelled = false
const run = async () => {
try {
const uid = item?.userId;
if (!uid || !userCache) return;
const cached = userCache.current.get(uid);
const uid = item?.userId
if (!uid || !userCache) return
const cached = userCache.current.get(uid)
if (cached) {
if (!cancelled) setOwner(cached);
return;
if (!cancelled) setOwner(cached)
return
}
const user = await getUserByUid?.(uid);
const user = await getUserByUid?.(uid)
if (!cancelled && user) {
userCache.current.set(uid, user);
setOwner(user);
userCache.current.set(uid, user)
setOwner(user)
}
} catch (_e) {}
};
run();
}
run()
return () => {
cancelled = true;
};
}, [item?.userId, userCache, getUserByUid]);
cancelled = true
}
}, [item?.userId, userCache, getUserByUid])
useEffect(() => {
const uid = item?.userId;
if (!uid) return;
const uid = item?.userId
if (!uid) return
const unsub = usersRef.doc(uid).onSnapshot(
(doc) => {
if (doc?.exists) {
const data = { id: doc.id, ...doc.data() };
setOwner(data);
const data = { id: doc.id, ...doc.data() }
setOwner(data)
try {
userCache?.current?.set(uid, data);
userCache?.current?.set(uid, data)
} catch (_e) {}
}
},
() => {}
);
return () => unsub?.();
}, [item?.userId, userCache]);
)
return () => unsub?.()
}, [item?.userId, userCache])
const [isFollowing, setIsFollowing] = useState(false);
const [isFollowing, setIsFollowing] = useState(false)
useEffect(() => {
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : [];
setIsFollowing(currentUID ? list.includes(currentUID) : false);
}, [owner?.followedBy, currentUID]);
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []
setIsFollowing(currentUID ? list.includes(currentUID) : false)
}, [owner?.followedBy, currentUID])
useEffect(() => {
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
setLikesCount(lb.length);
setIsLiked(currentUID ? lb.includes(currentUID) : false);
}, [item?.likes?.playback, currentUID]);
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
setLikesCount(lb.length)
setIsLiked(currentUID ? lb.includes(currentUID) : false)
}, [item?.likes?.playback, currentUID])
useEffect(() => {
startedRef.current = false;
}, [videoUrl]);
startedRef.current = false
}, [videoUrl])
// Start/reset when active, using the video's own audio
useEffect(() => {
const el = videoRef.current;
if (!el) return;
const el = videoRef.current
if (!el) return
if (!isActive) {
try {
if (!el.paused) el.pause();
if (!el.paused) el.pause()
} catch (_e) {}
try {
el.muted = true;
el.muted = true
} catch (_e) {}
startedRef.current = false;
onBackgroundSync?.({ isPlaying: false });
return;
startedRef.current = false
onBackgroundSync?.({ isPlaying: false })
return
}
const start = async () => {
try {
if (!startedRef.current) {
startedRef.current = true;
startedRef.current = true
const resetToStart = () => {
try {
el.currentTime = 0;
el.currentTime = 0
} catch (_e) {}
};
}
if (el.readyState >= 1) {
resetToStart();
resetToStart()
} else {
const handleLoaded = () => {
resetToStart();
};
el.addEventListener("loadeddata", handleLoaded, { once: true });
resetToStart()
}
el.addEventListener('loadeddata', handleLoaded, { once: true })
}
}
el.muted = false;
el.muted = false
await el.play().catch((error) => {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] play() rejected", error);
console.log('[PlaybackItem.web] play() rejected', error)
}
});
})
onBackgroundSync?.({
currentTime: Number(el.currentTime || 0),
isPlaying: !el.paused,
});
})
} catch (_e) {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] start error", _e);
console.log('[PlaybackItem.web] start error', _e)
}
}
};
}
start();
}, [isActive, videoUrl, onBackgroundSync]);
start()
}, [isActive, videoUrl, onBackgroundSync])
// Pause all on unmount
useEffect(() => {
const el = videoRef.current;
const el = videoRef.current
return () => {
try {
if (el && !el.paused) el.pause();
if (el && !el.paused) el.pause()
} catch (_e) {}
onBackgroundSync?.({ isPlaying: false });
};
}, [onBackgroundSync]);
onBackgroundSync?.({ isPlaying: false })
}
}, [onBackgroundSync])
// Lyrics timing based on video clock
const alignedWords = useMemo(() => {
const idx = Number(item?.songIndex) || 0;
const ts = item?.musicTimestamps?.[idx];
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
const idx = Number(item?.songIndex) || 0
const ts = item?.musicTimestamps?.[idx]
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
return arr.map((w) => ({
word: String(w?.word ?? ""),
word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
}));
}, [item?.musicTimestamps, item?.songIndex]);
}))
}, [item?.musicTimestamps, item?.songIndex])
const [currentTimeS, setCurrentTimeS] = useState(0);
const [currentTimeS, setCurrentTimeS] = useState(0)
useEffect(() => {
if (!isActive) return;
if (!isActive) return
const id = setInterval(() => {
try {
const el = videoRef.current;
const videoTime = Number(el?.currentTime || 0);
const safeTime = Number.isFinite(videoTime) ? videoTime : 0;
setCurrentTimeS(safeTime);
const el = videoRef.current
const videoTime = Number(el?.currentTime || 0)
const safeTime = Number.isFinite(videoTime) ? videoTime : 0
setCurrentTimeS(safeTime)
onBackgroundSync?.({
currentTime: safeTime,
isPlaying: !!el && !el.paused,
});
})
} catch (_e) {}
}, 250);
return () => clearInterval(id);
}, [isActive, onBackgroundSync]);
}, 250)
return () => clearInterval(id)
}, [isActive, onBackgroundSync])
useEffect(() => {
if (!onBackgroundSync) return undefined;
if (!onBackgroundSync) return undefined
if (isActive) {
wasActiveRef.current = true;
return undefined;
wasActiveRef.current = true
return undefined
}
if (wasActiveRef.current) {
onBackgroundSync({ isPlaying: false });
wasActiveRef.current = false;
onBackgroundSync({ isPlaying: false })
wasActiveRef.current = false
}
return undefined;
}, [isActive, onBackgroundSync]);
return undefined
}, [isActive, onBackgroundSync])
const descriptionText =
item?.description || item?.title || "Description chanson";
const descriptionText = item?.description || item?.title || 'Description chanson'
const creatorName = useMemo(() => {
if (owner?.displayName) return owner.displayName;
if (owner?.artistName) return owner.artistName;
if (owner?.userName) return owner.userName;
if (typeof item?.userName === "string") return item.userName;
return "";
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
if (owner?.displayName) return owner.displayName
if (owner?.artistName) return owner.artistName
if (owner?.userName) return owner.userName
if (typeof item?.userName === 'string') return item.userName
return ''
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName])
const sharePayload = useMemo(() => {
if (!item?.id) return null;
if (!item?.id) return null
return createPlaybackSharePayload({
projectId: item.id,
title: typeof item?.title === "string" ? item.title.trim() : undefined,
title: typeof item?.title === 'string' ? item.title.trim() : undefined,
artist: creatorName || undefined,
playbackUrl: videoUrl || undefined,
});
}, [creatorName, item?.id, item?.title, videoUrl]);
})
}, [creatorName, item?.id, item?.title, videoUrl])
const handleShare = useCallback(() => {
if (sharePayload) {
openShareSheet(sharePayload);
openShareSheet(sharePayload)
}
}, [sharePayload]);
}, [sharePayload])
const handleReport = useCallback(() => {
if (!item?.id) return;
SheetManager.show("Report", {
if (!item?.id) return
SheetManager.show('Report', {
payload: {
targetType: "playback",
targetType: 'playback',
projectId: item?.id || null,
playbackId: item?.id || null,
title: item?.title || "",
title: item?.title || '',
ownerId: item?.userId || null,
},
});
}, [item?.id, item?.title, item?.userId]);
})
}, [item?.id, item?.title, item?.userId])
const handleLayout = useCallback((event) => {
const { width = 0, height = 0 } = event?.nativeEvent?.layout || {};
const { width = 0, height = 0 } = event?.nativeEvent?.layout || {}
setLayoutSize((prev) => ({
width: width > 0 ? width : prev.width,
height: height > 0 ? height : prev.height,
}));
}, []);
}))
}, [])
const layoutWidth = layoutSize.width || viewportWidth;
const layoutHeight = layoutSize.height || viewportHeight;
const layoutWidth = layoutSize.width || viewportWidth
const layoutHeight = layoutSize.height || viewportHeight
const horizontalPadding = 64;
const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0);
const gapBetweenColumns = 40;
const minVideoWidth = 420;
const maxVideoWidth = 720;
const minCommentsWidth = 300;
const maxCommentsWidth = 420;
const horizontalPadding = 64
const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0)
const gapBetweenColumns = 40
const minVideoWidth = 420
const maxVideoWidth = 720
const minCommentsWidth = 300
const maxCommentsWidth = 420
let desiredHeight = Math.max(420, layoutHeight * 0.8);
let videoWidth = Math.min(
maxVideoWidth,
Math.max(minVideoWidth, innerWidth * 0.58)
);
let videoHeight = videoWidth * (16 / 9);
let desiredHeight = Math.max(420, layoutHeight * 0.8)
let videoWidth = Math.min(maxVideoWidth, Math.max(minVideoWidth, innerWidth * 0.58))
let videoHeight = videoWidth * (16 / 9)
if (videoHeight > desiredHeight) {
videoHeight = desiredHeight;
videoWidth = videoHeight * (9 / 16);
videoHeight = desiredHeight
videoWidth = videoHeight * (9 / 16)
}
let commentsWidth = Math.min(
maxCommentsWidth,
Math.max(minCommentsWidth, innerWidth - videoWidth - gapBetweenColumns)
);
const panelHeight = Math.min(videoHeight, desiredHeight);
)
const panelHeight = Math.min(videoHeight, desiredHeight)
// Attach verbose event listeners on the HTML video element
useEffect(() => {
const el = videoRef.current;
if (!el || !DEBUG_PLAYBACK_WEB) return undefined;
const el = videoRef.current
if (!el || !DEBUG_PLAYBACK_WEB) return undefined
const handler = (e) => {
// Avoid heavy logs: only show key events and brief state
console.log("[PlaybackItem.web] video:", e.type, {
console.log('[PlaybackItem.web] video:', e.type, {
t: Number(el.currentTime || 0).toFixed(2),
paused: el.paused,
rs: el.readyState,
});
};
})
}
const events = [
"loadedmetadata",
"loadeddata",
"play",
"playing",
"pause",
"seeking",
"seeked",
"stalled",
"waiting",
"ended",
"error",
];
events.forEach((ev) => el.addEventListener(ev, handler));
'loadedmetadata',
'loadeddata',
'play',
'playing',
'pause',
'seeking',
'seeked',
'stalled',
'waiting',
'ended',
'error',
]
events.forEach((ev) => el.addEventListener(ev, handler))
return () => {
events.forEach((ev) => el.removeEventListener(ev, handler));
};
}, []);
events.forEach((ev) => el.removeEventListener(ev, handler))
}
}, [])
return (
<View
@@ -368,18 +333,13 @@ const PlaybackItem = ({
styles.itemContainerBase,
styles.itemContainerRow,
{
height: "90%",
height: '90%',
paddingHorizontal: horizontalPadding,
paddingVertical: Math.max((layoutHeight - panelHeight) / 2, 24),
},
]}
>
<View
style={[
styles.videoColumn,
{ width: videoWidth, marginRight: gapBetweenColumns },
]}
>
<View style={[styles.videoColumn, { width: videoWidth, marginRight: gapBetweenColumns }]}>
<View style={[styles.videoSurface, { height: panelHeight }]}>
<View style={styles.videoContainer}>
{!!videoUrl ? (
@@ -392,12 +352,12 @@ const PlaybackItem = ({
preload="auto"
// Using web CSS properties here on purpose
style={{
position: "absolute",
position: 'absolute',
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block',
}}
/>
) : (
@@ -413,7 +373,7 @@ const PlaybackItem = ({
<Pressable
style={styles.ownerAvatarButton}
onPress={() => {
navigate(Routes.SingerProfile, { userId: item?.userId });
navigate(Routes.SingerProfile, { userId: item?.userId })
}}
>
{owner?.profilePictureURL && (
@@ -435,38 +395,32 @@ const PlaybackItem = ({
if (
!ensureAuthenticated(currentUID, {
onIntercept: () => {
setIsFollowing(false);
setIsFollowing(false)
},
})
) {
return;
return
}
try {
const next = !isFollowing;
setIsFollowing(next);
const next = !isFollowing
setIsFollowing(next)
setOwner((prev) => {
const fb = Array.isArray(prev?.followedBy)
? prev.followedBy
: [];
const fb = Array.isArray(prev?.followedBy) ? prev.followedBy : []
const newFb = next
? Array.from(new Set([...fb, currentUID]))
: fb.filter((x) => x !== currentUID);
return prev ? { ...prev, followedBy: newFb } : prev;
});
if (next) await followUser?.(owner.id);
else await unfollowUser?.(owner.id);
: fb.filter((x) => x !== currentUID)
return prev ? { ...prev, followedBy: newFb } : prev
})
if (next) await followUser?.(owner.id)
else await unfollowUser?.(owner.id)
} catch (_e) {
setIsFollowing((v) => !v);
setIsFollowing((v) => !v)
}
}}
>
<BlurView
tint="dark"
intensity={20}
style={styles.followButton}
>
<BlurView tint="dark" intensity={20} style={styles.followButton}>
<Text style={styles.followButtonText}>
{isFollowing ? "Suivi(e)" : "Suivre"}
{isFollowing ? 'Suivi(e)' : 'Suivre'}
</Text>
</BlurView>
</Pressable>
@@ -475,25 +429,23 @@ const PlaybackItem = ({
<Pressable
onPress={async () => {
try {
if (!item?.id) return;
if (!item?.id) return
if (!ensureAuthenticated(currentUID)) {
return;
return
}
const nextLiked = !isLiked;
setIsLiked(nextLiked);
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
const nextLiked = !isLiked
setIsLiked(nextLiked)
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)))
await toggleProjectLike({
projectId: item.id,
currentUID,
target: LIKE_TARGET.PLAYBACK,
next: nextLiked,
});
})
} catch (_e) {
setIsLiked((v) => !v);
setLikesCount((c) =>
isLiked ? c + 1 : Math.max(0, c - 1)
);
setIsLiked((v) => !v)
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)))
}
}}
style={[styles.actionButton, styles.actionSpacing]}
@@ -503,37 +455,16 @@ const PlaybackItem = ({
style={styles.actionIcon}
resizeMode="contain"
/>
{!!likesCount && (
<Text style={styles.actionLabel}>{likesCount}</Text>
)}
{!!likesCount && <Text style={styles.actionLabel}>{likesCount}</Text>}
</Pressable>
<Pressable
onPress={() => {}}
style={[styles.actionButton, styles.actionSpacing]}
>
<Image
source={icons.chatBubble}
style={styles.actionIcon}
resizeMode="contain"
/>
{!!commentsCount && (
<Text style={styles.actionLabel}>{commentsCount}</Text>
)}
<Pressable onPress={() => {}} style={[styles.actionButton, styles.actionSpacing]}>
<Image source={icons.chatBubble} style={styles.actionIcon} resizeMode="contain" />
{!!commentsCount && <Text style={styles.actionLabel}>{commentsCount}</Text>}
</Pressable>
<Pressable
onPress={handleShare}
style={[styles.actionButton, styles.actionSpacing]}
>
<Image
source={icons.share}
style={styles.actionIcon}
resizeMode="contain"
/>
<Pressable onPress={handleShare} style={[styles.actionButton, styles.actionSpacing]}>
<Image source={icons.share} style={styles.actionIcon} resizeMode="contain" />
</Pressable>
<Pressable
onPress={handleReport}
style={[styles.actionButton, styles.actionSpacing]}
>
<Pressable onPress={handleReport} style={[styles.actionButton, styles.actionSpacing]}>
<Feather name="flag" size={20} color={Palette.white} />
<Text style={styles.actionLabel}>Signaler</Text>
</Pressable>
@@ -542,10 +473,7 @@ const PlaybackItem = ({
<View style={styles.lyricsContainer}>
<BlurView tint="dark" intensity={20} style={styles.lyricsCard}>
{alignedWords?.length > 0 ? (
<KaraokeLyrics
alignedWords={alignedWords}
currentTimeS={currentTimeS}
/>
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={currentTimeS} />
) : (
<Text style={styles.lyricsText}>{descriptionText}</Text>
)}
@@ -556,52 +484,45 @@ const PlaybackItem = ({
</View>
{openComments && (
<View
style={[
styles.commentsColumn,
{ width: commentsWidth, height: panelHeight },
]}
>
<View style={[styles.commentsColumn, { width: commentsWidth, height: panelHeight }]}>
<CommentsPanel
projectId={item?.id}
description={descriptionText}
commentsCount={commentsCount}
onCommentAdded={() =>
setCommentsCount((c) => Math.max(0, Number(c || 0) + 1))
}
onCommentAdded={() => setCommentsCount((c) => Math.max(0, Number(c || 0) + 1))}
inputRef={commentInputRef}
panelHeight={panelHeight}
/>
</View>
)}
</View>
);
};
)
}
export default PlaybackItem;
export default PlaybackItem
const styles = StyleSheet.create({
itemContainerBase: {
width: "100%",
width: '100%',
paddingVertical: 36,
alignItems: "center",
alignItems: 'center',
},
itemContainerRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
videoColumn: {
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
videoSurface: {
alignSelf: "center",
alignSelf: 'center',
aspectRatio: 9 / 16,
borderRadius: 32,
overflow: "hidden",
backgroundColor: "#07040D",
shadowColor: "#000",
overflow: 'hidden',
backgroundColor: '#07040D',
shadowColor: '#000',
shadowOpacity: 0.45,
shadowRadius: 30,
shadowOffset: { width: 0, height: 20 },
@@ -609,16 +530,16 @@ const styles = StyleSheet.create({
},
videoContainer: {
flex: 1,
position: "relative",
position: 'relative',
},
actionStack: {
position: "absolute",
position: 'absolute',
right: 18,
top: 150,
alignItems: "center",
alignItems: 'center',
},
ownerBlock: {
alignItems: "center",
alignItems: 'center',
marginBottom: 26,
},
ownerAvatarButton: {
@@ -629,7 +550,7 @@ const styles = StyleSheet.create({
borderRadius: 100,
},
followPressable: {
alignSelf: "center",
alignSelf: 'center',
},
followButton: {
paddingVertical: 8,
@@ -637,8 +558,8 @@ const styles = StyleSheet.create({
borderRadius: 14,
borderWidth: 1,
borderColor: Palette.white,
backgroundColor: "#FFFFFF20",
overflow: "hidden",
backgroundColor: '#FFFFFF20',
overflow: 'hidden',
},
followButtonText: {
fontSize: 14,
@@ -646,8 +567,8 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterMedium,
},
actionButton: {
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
actionSpacing: {
marginTop: 24,
@@ -660,10 +581,10 @@ const styles = StyleSheet.create({
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
lyricsContainer: {
position: "absolute",
position: 'absolute',
left: 24,
right: 24,
bottom: 32,
@@ -673,7 +594,7 @@ const styles = StyleSheet.create({
paddingVertical: 12,
borderRadius: 22,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
},
lyricsText: {
fontSize: 14,
@@ -681,6 +602,6 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
},
commentsColumn: {
alignSelf: "stretch",
alignSelf: 'stretch',
},
});
})
@@ -1,27 +1,23 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Platform, StyleSheet, Text, View } from "react-native";
import KaraokeLyrics from "../../../components/KaraokeLyrics";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Platform, StyleSheet, Text, View } from 'react-native'
import KaraokeLyrics from '../../../components/KaraokeLyrics'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
const PlaybackLyricsCard = ({ alignedWords, currentTimeS, fallbackTitle }) => {
return (
<View style={styles.container}>
<BlurView
tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20}
style={styles.blur}
>
<BlurView tint="dark" intensity={Platform.OS !== 'ios' ? 10 : 20} style={styles.blur}>
{alignedWords?.length > 0 ? (
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={currentTimeS} />
) : (
<Text style={styles.title}>{fallbackTitle || "Description chanson"}</Text>
<Text style={styles.title}>{fallbackTitle || 'Description chanson'}</Text>
)}
</BlurView>
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
@@ -32,13 +28,13 @@ const styles = StyleSheet.create({
paddingVertical: 8,
borderRadius: 20,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
},
title: {
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
export default PlaybackLyricsCard;
export default PlaybackLyricsCard
@@ -1,7 +1,7 @@
import { VideoView } from "expo-video";
import React from "react";
import { Image, StyleSheet, View } from "react-native";
import { img } from "../../../assets";
import { VideoView } from 'expo-video'
import React from 'react'
import { Image, StyleSheet, View } from 'react-native'
import { img } from '../../../assets'
const PlaybackVideo = ({ videoUrl, videoPlayer }) => {
return (
@@ -16,19 +16,18 @@ const PlaybackVideo = ({ videoUrl, videoPlayer }) => {
) : (
<Image source={img.placeholder3} style={styles.fill} />
)}
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
},
fill: {
width: "100%",
height: "100%",
width: '100%',
height: '100%',
},
});
})
export default PlaybackVideo;
export default PlaybackVideo
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { usersRef } from "../../../config/firebase";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import { useCallback, useEffect, useState } from 'react'
import { usersRef } from '../../../config/firebase'
import { ensureAuthenticated } from '../../../utils/authRedirect'
const usePlaybackOwner = ({
item,
@@ -11,102 +11,93 @@ const usePlaybackOwner = ({
unfollowUser,
}) => {
const [owner, setOwner] = useState(
item?.userId && userCache?.current?.get(item.userId)
? userCache.current.get(item.userId)
: null
);
item?.userId && userCache?.current?.get(item.userId) ? userCache.current.get(item.userId) : null
)
useEffect(() => {
let cancelled = false;
let cancelled = false
const run = async () => {
try {
const uid = item?.userId;
if (!uid || !userCache) return;
const cached = userCache.current.get(uid);
const uid = item?.userId
if (!uid || !userCache) return
const cached = userCache.current.get(uid)
if (cached) {
if (!cancelled) setOwner(cached);
return;
if (!cancelled) setOwner(cached)
return
}
const user = await getUserByUid?.(uid);
const user = await getUserByUid?.(uid)
if (!cancelled && user) {
userCache.current.set(uid, user);
setOwner(user);
userCache.current.set(uid, user)
setOwner(user)
}
} catch (e) {}
};
run();
}
run()
return () => {
cancelled = true;
};
}, [item?.userId, userCache, getUserByUid]);
cancelled = true
}
}, [item?.userId, userCache, getUserByUid])
useEffect(() => {
const uid = item?.userId;
if (!uid) return;
const uid = item?.userId
if (!uid) return
const unsub = usersRef.doc(uid).onSnapshot(
(doc) => {
if (doc?.exists) {
const data = { id: doc.id, ...doc.data() };
setOwner(data);
const data = { id: doc.id, ...doc.data() }
setOwner(data)
try {
userCache?.current?.set(uid, data);
userCache?.current?.set(uid, data)
} catch (e) {}
}
},
() => {}
);
return () => unsub?.();
}, [item?.userId, userCache]);
)
return () => unsub?.()
}, [item?.userId, userCache])
const [isFollowing, setIsFollowing] = useState(false);
const [isFollowActionPending, setIsFollowActionPending] = useState(false);
const [isFollowing, setIsFollowing] = useState(false)
const [isFollowActionPending, setIsFollowActionPending] = useState(false)
useEffect(() => {
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : [];
setIsFollowing(currentUID ? list.includes(currentUID) : false);
}, [owner?.followedBy, currentUID]);
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []
setIsFollowing(currentUID ? list.includes(currentUID) : false)
}, [owner?.followedBy, currentUID])
const toggleFollow = useCallback(async () => {
if (!owner?.id || owner.id === currentUID) return;
if (isFollowActionPending) return;
if (!owner?.id || owner.id === currentUID) return
if (isFollowActionPending) return
if (
!ensureAuthenticated(currentUID, {
onIntercept: () => {
setIsFollowActionPending(false);
setIsFollowActionPending(false)
},
})
) {
return;
return
}
try {
setIsFollowActionPending(true);
const next = !isFollowing;
setIsFollowing(next);
setIsFollowActionPending(true)
const next = !isFollowing
setIsFollowing(next)
setOwner((prev) => {
const fb = Array.isArray(prev?.followedBy) ? prev.followedBy : [];
const fb = Array.isArray(prev?.followedBy) ? prev.followedBy : []
const newFb = next
? Array.from(new Set([...fb, currentUID]))
: fb.filter((x) => x !== currentUID);
return prev ? { ...prev, followedBy: newFb } : prev;
});
if (next) await followUser?.(owner.id);
else await unfollowUser?.(owner.id);
: fb.filter((x) => x !== currentUID)
return prev ? { ...prev, followedBy: newFb } : prev
})
if (next) await followUser?.(owner.id)
else await unfollowUser?.(owner.id)
global.setTimeout(() => {
setIsFollowActionPending(false);
}, 1000);
setIsFollowActionPending(false)
}, 1000)
} catch (e) {
setIsFollowing((v) => !v);
setIsFollowActionPending(false);
setIsFollowing((v) => !v)
setIsFollowActionPending(false)
}
}, [
currentUID,
followUser,
isFollowActionPending,
isFollowing,
owner?.id,
unfollowUser,
]);
}, [currentUID, followUser, isFollowActionPending, isFollowing, owner?.id, unfollowUser])
return { owner, isFollowing, isFollowActionPending, toggleFollow };
};
return { owner, isFollowing, isFollowActionPending, toggleFollow }
}
export default usePlaybackOwner;
export default usePlaybackOwner
@@ -1,50 +1,50 @@
import { useEffect, useMemo, useState } from "react";
import { useVideoPlayer } from "expo-video";
import { useEffect, useMemo, useState } from 'react'
import { useVideoPlayer } from 'expo-video'
const usePlaybackPlayer = ({ item, isActive }) => {
const baseVideoUrl = item?.playbackUrl || null;
const videoUrl = baseVideoUrl;
const baseVideoUrl = item?.playbackUrl || null
const videoUrl = baseVideoUrl
const videoPlayer = useVideoPlayer(videoUrl || null, (player) => {
player.loop = false;
player.muted = false;
player.timeUpdateEventInterval = 0.2;
});
player.loop = false
player.muted = false
player.timeUpdateEventInterval = 0.2
})
const shouldAutoPlay = isActive;
const shouldAutoPlay = isActive
useEffect(() => {
if (!videoPlayer) return;
if (!videoPlayer) return
if (shouldAutoPlay) {
try {
videoPlayer.currentTime = 0;
videoPlayer.play();
videoPlayer.currentTime = 0
videoPlayer.play()
} catch (e) {}
} else if (videoPlayer?.playing) {
try {
videoPlayer.pause();
videoPlayer.pause()
} catch (e) {}
}
}, [shouldAutoPlay, videoPlayer]);
}, [shouldAutoPlay, videoPlayer])
useEffect(() => {
return () => {
try {
if (videoPlayer?.playing) videoPlayer.pause();
if (videoPlayer?.playing) videoPlayer.pause()
} catch (e) {}
};
}, [videoPlayer]);
}
}, [videoPlayer])
const [currentTimeS, setCurrentTimeS] = useState(0);
const [currentTimeS, setCurrentTimeS] = useState(0)
useEffect(() => {
if (!isActive) return;
if (!isActive) return
const id = global.setInterval(() => {
try {
setCurrentTimeS(Number(videoPlayer?.currentTime || 0));
setCurrentTimeS(Number(videoPlayer?.currentTime || 0))
} catch (e) {}
}, 250);
return () => global.clearInterval(id);
}, [isActive, videoPlayer]);
}, 250)
return () => global.clearInterval(id)
}, [isActive, videoPlayer])
const playerState = useMemo(
() => ({
@@ -53,9 +53,9 @@ const usePlaybackPlayer = ({ item, isActive }) => {
currentTimeS,
}),
[videoPlayer, videoUrl, currentTimeS]
);
)
return playerState;
};
return playerState
}
export default usePlaybackPlayer;
export default usePlaybackPlayer
+9 -13
View File
@@ -1,9 +1,9 @@
import React from "reactn";
import { Text } from "react-native";
import React from 'reactn'
import { Text } from 'react-native'
import Page from "../layouts/Page";
import Page from '../layouts/Page'
import { Fonts } from "../styles";
import { Fonts } from '../styles'
const privacyPolicy = `Politique de Confidentialité de minuit.starter
@@ -45,16 +45,12 @@ Nous pouvons occasionnellement mettre à jour cette politique. Si des modificati
Si vous avez des questions concernant cette Politique de Confidentialité, contactez-nous à : hello@minuit.agency.
Dernière mise à jour : 06/08/2023.`;
Dernière mise à jour : 06/08/2023.`
export default ({} = {}) => {
return (
<Page
headerType="NAVIGATE"
title={"Politique de confidentialité"}
scrollEnabled
>
<Text style={{ ...Fonts({ type: "default" }) }}>{privacyPolicy}</Text>
<Page headerType="NAVIGATE" title={'Politique de confidentialité'} scrollEnabled>
<Text style={{ ...Fonts({ type: 'default' }) }}>{privacyPolicy}</Text>
</Page>
);
};
)
}
+39 -47
View File
@@ -1,54 +1,48 @@
import { View, Text, Image } from "react-native";
import React, { useState } from "react";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import { responsiveHeight } from "react-native-responsive-dimensions";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import Style, { size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { View, Text, Image } from 'react-native'
import React, { useState } from 'react'
import Page from '../../layouts/Page'
import { background, img } from '../../assets'
import MusicLandHeader from '../../components/MusicLandHeader'
import { goBack, navigate } from '../../navigation/NavigationService'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import Style, { size } from '../../styles/Style'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { Routes } from '../../navigation'
import { useRoute } from '@react-navigation/native'
const DownloadPrices = () => {
const params = useRoute().params;
const action = params?.action;
const [selectedIndex, setSelectedIndex] = useState(null);
const params = useRoute().params
const action = params?.action
const [selectedIndex, setSelectedIndex] = useState(null)
const PRICES = [
{
title: action === "playback" ? "1 Playback" : "Je télécharge ma chanson",
description: `Ce ${
action === "playback" ? "Playback" : "morceau"
} uniquement`,
price: "6,99€",
title: action === 'playback' ? '1 Playback' : 'Je télécharge ma chanson',
description: `Ce ${action === 'playback' ? 'Playback' : 'morceau'} uniquement`,
price: '6,99€',
},
{
title: `3 ${action === "playback" ? "Playback" : "chansons"}`,
title: `3 ${action === 'playback' ? 'Playback' : 'chansons'}`,
description: `Ce ${
action === "playback" ? "Playback" : "morceau"
action === 'playback' ? 'Playback' : 'morceau'
} et les 2 prochains que tu crées`,
price: "12,99€",
price: '12,99€',
},
{
title: `5 ${action === "playback" ? "Playback" : "chansons"}`,
title: `5 ${action === 'playback' ? 'Playback' : 'chansons'}`,
description: `Ce ${
action === "playback" ? "Playback" : "morceau"
action === 'playback' ? 'Playback' : 'morceau'
} et les 4 prochains que tu crées`,
price: "19,99€",
price: '19,99€',
recommended: true,
},
];
]
return (
<Page
backgroundImg={
action === "playback"
? background.playbackBG2
: background.productionBG2
}
backgroundImg={action === 'playback' ? background.playbackBG2 : background.productionBG2}
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={75} />
@@ -64,7 +58,7 @@ const DownloadPrices = () => {
style={{
...size({ size: 158 }),
borderRadius: 13,
alignSelf: "center",
alignSelf: 'center',
}}
/>
<View style={{ gap: 12, marginTop: 10 }}>
@@ -73,7 +67,7 @@ const DownloadPrices = () => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Tarifs
@@ -83,16 +77,14 @@ const DownloadPrices = () => {
<CreateLyricsHeader
key={index}
colors={
selectedIndex === index
? ["#F94697", "#7023F7"]
: [Palette.tran, Palette.tran]
selectedIndex === index ? ['#F94697', '#7023F7'] : [Palette.tran, Palette.tran]
}
tint={!item.recommended ? "dark" : "light"}
tint={!item.recommended ? 'dark' : 'light'}
onPress={() => {
setSelectedIndex(index);
setSelectedIndex(index)
navigate(Routes.SongDownloaded, {
action,
});
})
}}
>
<View style={{ paddingTop: 5 }}>
@@ -115,7 +107,7 @@ const DownloadPrices = () => {
{item.description}
</Text>
</View>
<View style={{ alignSelf: "flex-end", marginTop: 14 }}>
<View style={{ alignSelf: 'flex-end', marginTop: 14 }}>
<Text
style={{
fontSize: 12,
@@ -133,8 +125,8 @@ const DownloadPrices = () => {
height: 26,
...Style.containerCenter,
borderRadius: 100,
position: "absolute",
backgroundColor: "#FFFFFF33",
position: 'absolute',
backgroundColor: '#FFFFFF33',
bottom: 8,
left: 10,
}}
@@ -158,7 +150,7 @@ const DownloadPrices = () => {
</CreateLyricsHeader>
</View>
</Page>
);
};
)
}
export default DownloadPrices;
export default DownloadPrices
+226 -264
View File
@@ -1,353 +1,326 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Image,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background } from "../../assets";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import BorderGradientButton from "../../components/BorderGradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import AppCheckbox from "../../components/AppCheckbox";
import firebase, {
projectsRef,
serverTimestamp,
videosRef,
} from "../../config/firebase";
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUserData, useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import { getBlobForUrl, releaseBlobUrl } from "../../utils/blobUrlCache";
import ClubAdvantagesCard from "../Profile/components/ClubAdvantagesCard";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { Image as ExpoImage } from "expo-image";
import SubscriptionConfirmModal from "../../components/SubscriptionConfirmModal";
import { isWeb } from "../../hooks/useLayoutType";
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { background } from '../../assets'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import BorderGradientButton from '../../components/BorderGradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppCheckbox from '../../components/AppCheckbox'
import firebase, { projectsRef, serverTimestamp, videosRef } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUserData, useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { size } from '../../styles/Style'
import { getBlobForUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
import ClubAdvantagesCard from '../Profile/components/ClubAdvantagesCard'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { Image as ExpoImage } from 'expo-image'
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
import { isWeb } from '../../hooks/useLayoutType'
const triggerWebDownload = async (url, title) => {
if (Platform.OS !== "web") return;
if (!url) return;
if (typeof document === "undefined") return;
const baseName = (title || "Playback").toString().trim() || "Playback";
const sanitized = baseName.replace(/[\\/:*?"<>|]/g, "-");
const extension = guessExtension(url) || "mp4";
const filename = `${sanitized}.${extension}`;
if (Platform.OS !== 'web') return
if (!url) return
if (typeof document === 'undefined') return
const baseName = (title || 'Playback').toString().trim() || 'Playback'
const sanitized = baseName.replace(/[\\/:*?"<>|]/g, '-')
const extension = guessExtension(url) || 'mp4'
const filename = `${sanitized}.${extension}`
try {
const response = await fetch(url);
if (!response.ok || response.type === "opaque") {
throw new Error(`download_failed_${response.status || "opaque"}`);
const response = await fetch(url)
if (!response.ok || response.type === 'opaque') {
throw new Error(`download_failed_${response.status || 'opaque'}`)
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = blobUrl;
anchor.download = filename;
anchor.rel = "noopener noreferrer";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(blobUrl);
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = blobUrl
anchor.download = filename
anchor.rel = 'noopener noreferrer'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(blobUrl)
} catch (error) {
console.log("[PlaybackDownload] web download fallback", {
console.log('[PlaybackDownload] web download fallback', {
message: error?.message,
});
})
try {
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.rel = "noopener noreferrer";
anchor.target = "_blank";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
return;
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
anchor.rel = 'noopener noreferrer'
anchor.target = '_blank'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
return
} catch (fallbackError) {
console.log("[PlaybackDownload] anchor fallback failed", {
console.log('[PlaybackDownload] anchor fallback failed', {
message: fallbackError?.message,
});
})
}
try {
window.open(url, "_blank", "noopener,noreferrer");
} catch { }
window.open(url, '_blank', 'noopener,noreferrer')
} catch {}
}
};
}
const guessExtension = (inputUri = "") => {
const cleaned = inputUri.split("?")[0] || "";
const match = cleaned.match(/\.([a-z0-9]+)$/i);
if (match && match[1]) return match[1].toLowerCase();
if (Platform.OS === "web") return "webm";
return "mp4";
};
const guessExtension = (inputUri = '') => {
const cleaned = inputUri.split('?')[0] || ''
const match = cleaned.match(/\.([a-z0-9]+)$/i)
if (match && match[1]) return match[1].toLowerCase()
if (Platform.OS === 'web') return 'webm'
return 'mp4'
}
const uploadSourceRecording = async ({ uri, uid, projectId }) => {
console.log("[PlaybackDownload] uploadSourceRecording params", {
console.log('[PlaybackDownload] uploadSourceRecording params', {
hasUri: Boolean(uri),
uid,
projectId,
});
})
if (!uri) throw new Error("Aucune vidéo trouvée");
if (!uid) throw new Error("Utilisateur non authentifié");
if (!uri) throw new Error('Aucune vidéo trouvée')
if (!uid) throw new Error('Utilisateur non authentifié')
const extension = guessExtension(uri);
const sourcePath = `users/${uid}/projects/${projectId}/recordings/source-${Date.now()}.${extension}`;
console.log("source path : ", sourcePath);
console.log("[PlaybackDownload] uploadSourceRecording source path", {
const extension = guessExtension(uri)
const sourcePath = `users/${uid}/projects/${projectId}/recordings/source-${Date.now()}.${extension}`
console.log('source path : ', sourcePath)
console.log('[PlaybackDownload] uploadSourceRecording source path', {
extension,
sourcePath,
});
})
const cachedBlob = getBlobForUrl(uri);
console.log("[PlaybackDownload] uploadSourceRecording blob cache", {
const cachedBlob = getBlobForUrl(uri)
console.log('[PlaybackDownload] uploadSourceRecording blob cache', {
hasBlob: Boolean(cachedBlob),
});
})
const { resultURI: videoUrl } = await uploadFileToFirebase({
uri,
path: sourcePath,
shouldCompress: true,
fileType: "VIDEO",
fileType: 'VIDEO',
blob: cachedBlob || undefined,
});
})
return { sourcePath, videoUrl };
};
return { sourcePath, videoUrl }
}
const PlaybackDownload = ({ route }) => {
const { currentUID } = useUserData();
const { hasActiveSubscription } = useUser() || {};
const { action, uri, project } = route.params || {};
console.log("[PlaybackDownload] route params", {
const { currentUID } = useUserData()
const { hasActiveSubscription } = useUser() || {}
const { action, uri, project } = route.params || {}
console.log('[PlaybackDownload] route params', {
action,
projectId: project?.id,
hasUri: Boolean(uri),
currentUID,
});
const { setIsLoading, setTooltip } = useMinuit();
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] =
useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(
project?.playbackUrl || null,
);
const [isPublishing, setIsPublishing] = useState(false);
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
const hasShownAfterPlaybackRef = useRef(false);
})
const { setIsLoading, setTooltip } = useMinuit()
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null)
const [isPublishing, setIsPublishing] = useState(false)
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
const hasShownAfterPlaybackRef = useRef(false)
const { data: video } = useDataFromRef({
ref: videosRef.doc("fr"),
ref: videosRef.doc('fr'),
simpleRef: true,
});
})
const afterPlaybackUrl = useMemo(() => {
if (!video) return null;
return video?.afterPlayback;
}, [video]);
if (!video) return null
return video?.afterPlayback
}, [video])
useEffect(() => {
if (action !== "playback") return;
if (!afterPlaybackUrl) return;
if (hasShownAfterPlaybackRef.current) return;
hasShownAfterPlaybackRef.current = true;
setIsAfterPlaybackVideoVisible(true);
}, [action, afterPlaybackUrl]);
if (action !== 'playback') return
if (!afterPlaybackUrl) return
if (hasShownAfterPlaybackRef.current) return
hasShownAfterPlaybackRef.current = true
setIsAfterPlaybackVideoVisible(true)
}, [action, afterPlaybackUrl])
useEffect(() => {
if (project?.playbackUrl) {
setPendingPlaybackUrl(project.playbackUrl);
setPendingPlaybackUrl(project.playbackUrl)
}
}, [project?.playbackUrl]);
}, [project?.playbackUrl])
useEffect(() => {
return () => {
releaseBlobUrl(uri || null);
};
}, [uri]);
releaseBlobUrl(uri || null)
}
}, [uri])
const handleDownloadUri = async () => {
if (isPublishing) return;
if (action === "playback" && project?.id) {
if (isPublishing) return
if (action === 'playback' && project?.id) {
if (pendingPlaybackUrl) {
await triggerWebDownload(pendingPlaybackUrl, project?.title);
return;
await triggerWebDownload(pendingPlaybackUrl, project?.title)
return
}
// Publication du playback
console.log("[PlaybackDownload] handleDownloadUri playback", {
console.log('[PlaybackDownload] handleDownloadUri playback', {
projectId: project.id,
uri,
currentUID,
});
const audioUrl = project?.songUrl || null;
})
const audioUrl = project?.songUrl || null
if (!audioUrl) {
setTooltip({
type: "error",
text: "Aucune piste audio disponible pour ce projet",
});
return;
type: 'error',
text: 'Aucune piste audio disponible pour ce projet',
})
return
}
let tempSourcePath = null;
let tempSourcePath = null
try {
setIsLoading(true);
setIsLoading(true)
const { sourcePath, videoUrl } = await uploadSourceRecording({
uri,
uid: currentUID,
projectId: project.id,
});
tempSourcePath = sourcePath;
})
tempSourcePath = sourcePath
const callable = firebase
.functions()
.httpsCallable("upload-mergeVideoAndAudio");
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = {
projectId: project?.id,
videoUrl,
audioUrl,
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
};
}
console.log(
"[PlaybackDownload] calling upload-mergeVideoAndAudio",
payload,
);
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
const { data: result } = await callable(payload);
const { data: result } = await callable(payload)
console.log(
"[PlaybackDownload] upload-mergeVideoAndAudio result",
result,
);
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result)
const resultURI = result?.url || null;
const resultURI = result?.url || null
if (resultURI) {
setPendingPlaybackUrl(resultURI);
setPendingPlaybackUrl(resultURI)
setTooltip({
type: "success",
text: "Playback prêt à télécharger",
});
await triggerWebDownload(resultURI, project?.title);
type: 'success',
text: 'Playback prêt à télécharger',
})
await triggerWebDownload(resultURI, project?.title)
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete();
await firebase.storage().ref(tempSourcePath).delete()
} catch (cleanupError) {
console.log("[PlaybackDownload] unable to delete temp source", {
console.log('[PlaybackDownload] unable to delete temp source', {
message: cleanupError?.message,
code: cleanupError?.code,
});
})
}
}
} else {
setTooltip({
type: "error",
text: "Erreur lors de la publication du playback",
});
type: 'error',
text: 'Erreur lors de la publication du playback',
})
}
} catch (error) {
console.log("[PlaybackDownload] error upload playback", {
console.log('[PlaybackDownload] error upload playback', {
message: error?.message,
code: error?.code,
name: error?.name,
details: error?.details,
});
})
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete();
} catch { }
await firebase.storage().ref(tempSourcePath).delete()
} catch {}
}
setTooltip({
type: "error",
text: String(
error?.message || "Erreur lors de la publication du playback",
),
});
type: 'error',
text: String(error?.message || 'Erreur lors de la publication du playback'),
})
} finally {
releaseBlobUrl(uri || null);
setIsLoading(false);
releaseBlobUrl(uri || null)
setIsLoading(false)
}
// Handle playback download
} else {
// Handle song download
}
};
}
const handlePublish = async () => {
if (isPublishing) return;
if (isPublishing) return
if (!hasAcceptedPublication) {
setTooltip({
type: "error",
text: "Confirme la diffusion sur Musicland et YouTube avant de publier",
});
return;
type: 'error',
text: 'Confirme la diffusion sur Musicland et YouTube avant de publier',
})
return
}
if (!project?.id) {
if (action === "playback") {
if (action === 'playback') {
setTooltip({
type: "success",
text: "Playback publié !",
});
navigate(Routes.Home);
type: 'success',
text: 'Playback publié !',
})
navigate(Routes.Home)
} else {
navigate(Routes.SongRelease, { action });
navigate(Routes.SongRelease, { action })
}
return;
return
}
let tempSourcePath = null;
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null;
const audioUrl = project?.songUrl || null;
let tempSourcePath = null
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null
const audioUrl = project?.songUrl || null
if (!audioUrl && !playbackUrlToSave) {
setTooltip({
type: "error",
text: "Aucune piste audio disponible pour ce projet",
});
return;
type: 'error',
text: 'Aucune piste audio disponible pour ce projet',
})
return
}
try {
setIsPublishing(true);
setIsLoading(true);
setIsPublishing(true)
setIsLoading(true)
if (!playbackUrlToSave) {
const { sourcePath, videoUrl } = await uploadSourceRecording({
uri,
uid: currentUID,
projectId: project.id,
});
tempSourcePath = sourcePath;
})
tempSourcePath = sourcePath
const callable = firebase
.functions()
.httpsCallable("upload-mergeVideoAndAudio");
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = {
projectId: project?.id,
videoUrl,
audioUrl,
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
};
console.log("[PlaybackDownload] publish: calling merge", payload);
const { data: result } = await callable(payload);
playbackUrlToSave = result?.url || null;
if (!playbackUrlToSave) {
throw new Error("merge_failed");
}
setPendingPlaybackUrl(playbackUrlToSave);
console.log('[PlaybackDownload] publish: calling merge', payload)
const { data: result } = await callable(payload)
playbackUrlToSave = result?.url || null
if (!playbackUrlToSave) {
throw new Error('merge_failed')
}
setPendingPlaybackUrl(playbackUrlToSave)
}
await projectsRef.doc(project.id).set(
@@ -355,47 +328,45 @@ const PlaybackDownload = ({ route }) => {
playbackUrl: playbackUrlToSave,
updatedAt: serverTimestamp(),
},
{ merge: true },
);
{ merge: true }
)
if (action === "playback") {
if (action === 'playback') {
setTooltip({
type: "success",
text: "Playback publié !",
});
navigate(Routes.Home);
type: 'success',
text: 'Playback publié !',
})
navigate(Routes.Home)
} else {
navigate(Routes.SongRelease, { action });
navigate(Routes.SongRelease, { action })
}
} catch (error) {
console.log("[PlaybackDownload] publish error", {
console.log('[PlaybackDownload] publish error', {
message: error?.message,
code: error?.code,
name: error?.name,
details: error?.details,
});
})
setTooltip({
type: "error",
text: String(error?.message || "Publication impossible"),
});
type: 'error',
text: String(error?.message || 'Publication impossible'),
})
} finally {
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete();
await firebase.storage().ref(tempSourcePath).delete()
} catch (cleanupError) {
console.log("[PlaybackDownload] unable to delete temp source", {
console.log('[PlaybackDownload] unable to delete temp source', {
message: cleanupError?.message,
code: cleanupError?.code,
});
})
}
}
setIsPublishing(false);
setIsLoading(false);
setIsPublishing(false)
setIsLoading(false)
}
};
const continueLabel = hasActiveSubscription
? "Publier"
: "Publier sans générer de revenus";
}
const continueLabel = hasActiveSubscription ? 'Publier' : 'Publier sans générer de revenus'
return (
<>
<Page
@@ -408,11 +379,8 @@ const PlaybackDownload = ({ route }) => {
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={50} />
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
<CreateLyricsHeader title={"Publier ton playback"} />
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<CreateLyricsHeader title={'Publier ton playback'} />
<View style={styles.coverRow}>
{project?.coverUrl ? (
@@ -428,11 +396,7 @@ const PlaybackDownload = ({ route }) => {
)}
<Pressable style={styles.downloadTile} onPress={handleDownloadUri}>
<MaterialCommunityIcons
name="download"
size={22}
color={Palette.white}
/>
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
<Text style={styles.downloadText}>Télécharger le playback</Text>
</Pressable>
</View>
@@ -442,9 +406,7 @@ const PlaybackDownload = ({ route }) => {
<View style={styles.publishConsentContainer}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() =>
setHasAcceptedPublication((prevState) => !prevState)
}
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
label="J'accepte la diffusion de mon playback sur Musicland et YouTube."
/>
<Text style={styles.publishConsentDescription}>
@@ -456,9 +418,9 @@ const PlaybackDownload = ({ route }) => {
title={continueLabel}
onPress={() => {
if (hasActiveSubscription) {
handlePublish();
handlePublish()
} else {
setShowConfirmModal(true);
setShowConfirmModal(true)
}
}}
disabled={isPublishing || !hasAcceptedPublication}
@@ -467,17 +429,17 @@ const PlaybackDownload = ({ route }) => {
</ScrollView>
</Page>
<SubscriptionConfirmModal
continueLabel={"Continuer vers la publication de playback"}
continueLabel={'Continuer vers la publication de playback'}
isVisible={showConfirmModal}
setIsVisible={setShowConfirmModal}
onJoinClub={() => navigate(Routes.Payments)}
onContinue={handlePublish}
/>
</>
);
};
)
}
export default PlaybackDownload;
export default PlaybackDownload
const styles = StyleSheet.create({
container: {
@@ -487,25 +449,25 @@ const styles = StyleSheet.create({
gap: 14,
},
coverRow: {
flexDirection: isWeb ? "row" : "column",
alignItems: "center",
justifyContent: isWeb ? "center" : "center",
flexDirection: isWeb ? 'row' : 'column',
alignItems: 'center',
justifyContent: isWeb ? 'center' : 'center',
gap: 12,
},
coverImage: {
...size({ size: 140 }),
borderRadius: 14,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.18)",
borderColor: 'rgba(255, 255, 255, 0.18)',
},
downloadTile: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 14,
backgroundColor: "#8C4BFF",
backgroundColor: '#8C4BFF',
},
downloadText: {
fontFamily: FONT_FAMILY.InterSemiBold,
@@ -528,4 +490,4 @@ const styles = StyleSheet.create({
continueButton: {
marginTop: 10,
},
});
})
+19 -21
View File
@@ -1,30 +1,28 @@
import { View, Text, Image } from "react-native";
import React from "react";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import Slider from "../../components/Slider";
import { gutters } from "../../styles";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import { View, Text, Image } from 'react-native'
import React from 'react'
import Page from '../../layouts/Page'
import { background, img } from '../../assets'
import MusicLandHeader from '../../components/MusicLandHeader'
import { goBack, navigate } from '../../navigation/NavigationService'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import Slider from '../../components/Slider'
import { gutters } from '../../styles'
import GradientButton from '../../components/GradientButton'
import { Routes } from '../../navigation'
const PlaybackExample = () => {
return (
<Page backgroundImg={background.productionBG} headerType="NONE">
<MusicLandHeader progress={50} onPressBack={goBack} />
<View
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}>
<CreateLyricsHeader title="Exemple" subTitle="Exemple de Playback" />
<View style={{ flex: 1, gap: 22 }}>
<Image
source={img.placeholder3}
style={{
width: "80%",
width: '80%',
flex: 1,
alignSelf: "center",
alignSelf: 'center',
borderRadius: 16,
}}
/>
@@ -33,15 +31,15 @@ const PlaybackExample = () => {
<GradientButton
title="Je veux devenir Playbacker"
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
marginTop: 4,
}}
onPress={() => navigate(Routes.PlaybackOnboarding)}
/>
</View>
</Page>
);
};
)
}
export default PlaybackExample;
export default PlaybackExample
+14 -14
View File
@@ -1,13 +1,13 @@
import { View } from "react-native";
import React from "react";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { gutters } from "../../styles";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import BorderGradientButton from "../../components/BorderGradientButton";
import { Routes } from "../../navigation";
import { View } from 'react-native'
import React from 'react'
import Page from '../../layouts/Page'
import { background } from '../../assets'
import { gutters } from '../../styles'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import { goBack, navigate } from '../../navigation/NavigationService'
import BorderGradientButton from '../../components/BorderGradientButton'
import { Routes } from '../../navigation'
const Production = () => {
return (
@@ -17,7 +17,7 @@ const Production = () => {
style={{
flex: 1,
paddingBottom: gutters * 2,
justifyContent: "flex-end",
justifyContent: 'flex-end',
gap: 37,
}}
>
@@ -38,7 +38,7 @@ const Production = () => {
</View>
</View>
</Page>
);
};
)
}
export default Production;
export default Production
+13 -16
View File
@@ -1,11 +1,11 @@
import { View, Text } from "react-native";
import React from "react";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { gutters } from "../../styles";
import GradientButton from "../../components/GradientButton";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import { View, Text } from 'react-native'
import React from 'react'
import Page from '../../layouts/Page'
import { background } from '../../assets'
import { gutters } from '../../styles'
import GradientButton from '../../components/GradientButton'
import { navigate } from '../../navigation/NavigationService'
import { Routes } from '../../navigation'
const ProductionOnboarding = () => {
return (
@@ -14,16 +14,13 @@ const ProductionOnboarding = () => {
style={{
flex: 1,
paddingBottom: gutters * 2,
justifyContent: "flex-end",
justifyContent: 'flex-end',
}}
>
<GradientButton
title="Commence"
onPress={() => navigate(Routes.Production)}
/>
<GradientButton title="Commence" onPress={() => navigate(Routes.Production)} />
</View>
</Page>
);
};
)
}
export default ProductionOnboarding;
export default ProductionOnboarding
+35 -41
View File
@@ -1,38 +1,36 @@
import { View, Text, Image } from 'react-native';
import React from 'react';
import Page from '../../layouts/Page';
import { background, img } from '../../assets';
import { goBack, navigate } from '../../navigation/NavigationService';
import MusicLandHeader from '../../components/MusicLandHeader';
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader';
import { size } from '../../styles/Style';
import BorderGradientButton from '../../components/BorderGradientButton';
import GradientButton from '../../components/GradientButton';
import { responsiveHeight } from 'react-native-responsive-dimensions';
import { Palette } from '../../styles';
import { FONT_FAMILY } from '../../styles/Fonts';
import { Routes } from '../../navigation';
import { useRoute } from '@react-navigation/native';
import { View, Text, Image } from 'react-native'
import React from 'react'
import Page from '../../layouts/Page'
import { background, img } from '../../assets'
import { goBack, navigate } from '../../navigation/NavigationService'
import MusicLandHeader from '../../components/MusicLandHeader'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { size } from '../../styles/Style'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { Routes } from '../../navigation'
import { useRoute } from '@react-navigation/native'
const SongDownloaded = () => {
const params = useRoute().params;
const action = params?.action;
const params = useRoute().params
const action = params?.action
return (
<Page
backgroundImg={
action === 'playback'
? background.playbackBG2
: background.productionBG2
}
headerType='NONE'>
backgroundImg={action === 'playback' ? background.playbackBG2 : background.productionBG2}
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={100} />
<View style={{ flex: 1, paddingTop: responsiveHeight(15) }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}>
}}
>
<Image
source={img.placeholder2}
style={{
@@ -48,7 +46,8 @@ const SongDownloaded = () => {
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: 'center',
}}>
}}
>
{action === 'playback' ? 'Playback' : 'Chanson'} téléchargée !
</Text>
<Text
@@ -56,10 +55,10 @@ const SongDownloaded = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}>
{action === 'playback' ? 'Ton Playback' : 'Ta Chanson'} est
désormais téléchargée dans tes fichiers. N’hésite pas à la
partager avec tes proches.
}}
>
{action === 'playback' ? 'Ton Playback' : 'Ta Chanson'} est désormais téléchargée dans
tes fichiers. N’hésite pas à la partager avec tes proches.
</Text>
</View>
<View
@@ -69,20 +68,15 @@ const SongDownloaded = () => {
paddingBottom: 12,
width: '80%',
alignSelf: 'center',
}}>
<BorderGradientButton
title='Partager'
onPress={() => navigate(Routes.Production)}
/>
<GradientButton
title="Page d'accueil"
onPress={() => navigate(Routes.BottomTab)}
/>
}}
>
<BorderGradientButton title="Partager" onPress={() => navigate(Routes.Production)} />
<GradientButton title="Page d'accueil" onPress={() => navigate(Routes.BottomTab)} />
</View>
</CreateLyricsHeader>
</View>
</Page>
);
};
)
}
export default SongDownloaded;
export default SongDownloaded
+31 -41
View File
@@ -1,30 +1,26 @@
import { View, Text, Image } from "react-native";
import React from "react";
import MusicLandHeader from "../../components/MusicLandHeader";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
import { goBack, navigate } from "../../navigation/NavigationService";
import { responsiveHeight } from "react-native-responsive-dimensions";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { View, Text, Image } from 'react-native'
import React from 'react'
import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page'
import { background, img } from '../../assets'
import { goBack, navigate } from '../../navigation/NavigationService'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { size } from '../../styles/Style'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import { Routes } from '../../navigation'
import { useRoute } from '@react-navigation/native'
const SongRelease = () => {
const params = useRoute().params;
const action = params?.action;
const params = useRoute().params
const action = params?.action
return (
<Page
backgroundImg={
action === "playback"
? background.playbackBG2
: background.productionBG2
}
backgroundImg={action === 'playback' ? background.playbackBG2 : background.productionBG2}
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={100} />
@@ -40,7 +36,7 @@ const SongRelease = () => {
style={{
...size({ size: 158 }),
borderRadius: 13,
alignSelf: "center",
alignSelf: 'center',
}}
/>
<View style={{ gap: 2, marginTop: 12 }}>
@@ -49,10 +45,10 @@ const SongRelease = () => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
{action === "playback" ? "Playback" : "Chanson"} publiée !
{action === 'playback' ? 'Playback' : 'Chanson'} publiée !
</Text>
<Text
style={{
@@ -61,9 +57,9 @@ const SongRelease = () => {
fontFamily: FONT_FAMILY.InterMedium,
}}
>
{action === "playback" ? "Ton Playback" : "Ta Chanson"} est
désormais accessible sur l’application et les réseaux sociaux de
MusicLand. N’hésite pas à la partager avec tes proches.
{action === 'playback' ? 'Ton Playback' : 'Ta Chanson'} est désormais accessible sur
l’application et les réseaux sociaux de MusicLand. N’hésite pas à la partager avec tes
proches.
</Text>
</View>
<View
@@ -71,23 +67,17 @@ const SongRelease = () => {
marginTop: 26,
gap: 12,
paddingBottom: 12,
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
>
<BorderGradientButton
title="Partager"
onPress={() => navigate(Routes.Production)}
/>
<GradientButton
title="Page d'accueil"
onPress={() => navigate(Routes.Production)}
/>
<BorderGradientButton title="Partager" onPress={() => navigate(Routes.Production)} />
<GradientButton title="Page d'accueil" onPress={() => navigate(Routes.Production)} />
</View>
</CreateLyricsHeader>
</View>
</Page>
);
};
)
}
export default SongRelease;
export default SongRelease
+59 -65
View File
@@ -1,73 +1,69 @@
import { View, Text, Image } from "react-native";
import React, { useCallback, useEffect, useState } from "react";
import AppCheckbox from "../../components/AppCheckbox";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton";
import { background, img } from "../../assets";
import { goBack, navigate } from "../../navigation/NavigationService";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { useUser } from "../../providers/UserDataProvider";
import { View, Text, Image } from 'react-native'
import React, { useCallback, useEffect, useState } from 'react'
import AppCheckbox from '../../components/AppCheckbox'
import Page from '../../layouts/Page'
import MusicLandHeader from '../../components/MusicLandHeader'
import GradientButton from '../../components/GradientButton'
import { background, img } from '../../assets'
import { goBack, navigate } from '../../navigation/NavigationService'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { size } from '../../styles/Style'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { Routes } from '../../navigation'
import { useRoute } from '@react-navigation/native'
import { useUser } from '../../providers/UserDataProvider'
const StreamSong = () => {
const params = useRoute().params;
const action = params?.action;
const project = params?.project || null;
const { hasActiveSubscription } = useUser() || {};
const userHasActiveSubscription = hasActiveSubscription;
const { setTooltip } = useMinuit();
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
const params = useRoute().params
const action = params?.action
const project = params?.project || null
const { hasActiveSubscription } = useUser() || {}
const userHasActiveSubscription = hasActiveSubscription
const { setTooltip } = useMinuit()
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
useEffect(() => {
if (action && action !== "playback") {
navigate(Routes.SongDownload, { project });
if (action && action !== 'playback') {
navigate(Routes.SongDownload, { project })
}
}, [action, project]);
}, [action, project])
const headingText = "Publier ton playback";
const headingText = 'Publier ton playback'
const descriptionText = userHasActiveSubscription
? "Ton abonnement est actif, publie ton playback et participe au concours."
: "Pour publier ton playback sur la plateforme, rejoins le Club MusicLand.";
? 'Ton abonnement est actif, publie ton playback et participe au concours.'
: 'Pour publier ton playback sur la plateforme, rejoins le Club MusicLand.'
const ctaLabel = userHasActiveSubscription
? "Publier mon playback"
: "Rejoindre le Club MusicLand";
? 'Publier mon playback'
: 'Rejoindre le Club MusicLand'
const handlePrimaryAction = useCallback(() => {
if (!hasAcceptedPublication) {
setTooltip({
type: "error",
text: "Confirme la diffusion sur Musicland et YouTube avant de publier",
});
return;
type: 'error',
text: 'Confirme la diffusion sur Musicland et YouTube avant de publier',
})
return
}
if (userHasActiveSubscription) {
if (action === "playback") {
if (action === 'playback') {
setTooltip({
type: "success",
text: "Playback publié !",
});
navigate(Routes.Home);
return;
type: 'success',
text: 'Playback publié !',
})
navigate(Routes.Home)
return
}
navigate(Routes.SongRelease, { action });
return;
navigate(Routes.SongRelease, { action })
return
}
navigate(Routes.Payments);
}, [action, hasAcceptedPublication, setTooltip, userHasActiveSubscription]);
navigate(Routes.Payments)
}, [action, hasAcceptedPublication, setTooltip, userHasActiveSubscription])
return (
<Page
backgroundImg={
action === "playback"
? background.playbackBG2
: background.productionBG2
}
backgroundImg={action === 'playback' ? background.playbackBG2 : background.productionBG2}
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={75} />
@@ -83,7 +79,7 @@ const StreamSong = () => {
style={{
...size({ size: 158 }),
borderRadius: 13,
alignSelf: "center",
alignSelf: 'center',
}}
/>
<View style={{ gap: 12 }}>
@@ -93,7 +89,7 @@ const StreamSong = () => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
{headingText}
@@ -103,7 +99,7 @@ const StreamSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
>
{descriptionText}
@@ -115,9 +111,7 @@ const StreamSong = () => {
<View style={{ gap: 6 }}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() =>
setHasAcceptedPublication((prevState) => !prevState)
}
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
/>
<Text
@@ -135,9 +129,9 @@ const StreamSong = () => {
title={ctaLabel}
onPress={handlePrimaryAction}
containerStyle={{
width: "100%",
width: '100%',
maxWidth: 320,
alignSelf: "center",
alignSelf: 'center',
}}
/>
{!userHasActiveSubscription && (
@@ -146,18 +140,18 @@ const StreamSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
opacity: 0.8,
}}
>
Rejoins le club pour débloquer la publication de tes créations et
tenter de grimper dans le classement.
Rejoins le club pour débloquer la publication de tes créations et tenter de grimper
dans le classement.
</Text>
)}
</View>
</View>
</Page>
);
};
)
}
export default StreamSong;
export default StreamSong
+64 -67
View File
@@ -1,87 +1,84 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useState } from "react";
import { Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { checkIfEmailIsValid } from "../../actions/signupActions";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import firebase, { usersRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import EditInput from "./components/EditInput";
import { BlurView } from 'expo-blur'
import React, { useEffect, useState } from 'react'
import { Platform, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { checkIfEmailIsValid } from '../../actions/signupActions'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import firebase, { usersRef } from '../../config/firebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import EditInput from './components/EditInput'
const ChangeEmailAddress = () => {
const [email, setEmail] = useState("");
const [originalEmail, setOriginalEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [, setTooltip] = useGlobal("_tooltip");
const [currentUserData] = useGlobal("currentUserData");
const [loading, setLoading] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
const [email, setEmail] = useState('')
const [originalEmail, setOriginalEmail] = useState('')
const [emailError, setEmailError] = useState('')
const [, setTooltip] = useGlobal('_tooltip')
const [currentUserData] = useGlobal('currentUserData')
const [loading, setLoading] = useState(false)
const [currentPassword, setCurrentPassword] = useState('')
useEffect(() => {
const authEmail = firebase.auth().currentUser?.email || "";
const profileEmail = (currentUserData?.email || authEmail || "").trim();
setEmail((prev) => (prev ? prev : profileEmail));
setOriginalEmail(profileEmail);
}, [currentUserData?.email]);
const authEmail = firebase.auth().currentUser?.email || ''
const profileEmail = (currentUserData?.email || authEmail || '').trim()
setEmail((prev) => (prev ? prev : profileEmail))
setOriginalEmail(profileEmail)
}, [currentUserData?.email])
const onChangeEmail = (val) => {
setEmail(val);
const trimmed = (val || "").trim();
setEmail(val)
const trimmed = (val || '').trim()
if (trimmed.length === 0) {
setEmailError("");
return;
setEmailError('')
return
}
if (!checkIfEmailIsValid({ email: trimmed })) {
setEmailError("Adresse email invalide");
setEmailError('Adresse email invalide')
} else {
setEmailError("");
setEmailError('')
}
};
}
const onSave = async () => {
const val = (email || "").trim();
if (!val) return;
const val = (email || '').trim()
if (!val) return
try {
setLoading(true);
const user = firebase.auth().currentUser;
const uid = user?.uid;
if (!user || !uid) return;
setLoading(true)
const user = firebase.auth().currentUser
const uid = user?.uid
if (!user || !uid) return
// Reauthenticate user with current password before sensitive update
try {
const credential = firebase.auth.EmailAuthProvider.credential(
user.email,
currentPassword
);
await user.reauthenticateWithCredential(credential);
const credential = firebase.auth.EmailAuthProvider.credential(user.email, currentPassword)
await user.reauthenticateWithCredential(credential)
} catch (reauthErr) {
throw {
code: "auth/wrong-password",
message: "Mot de passe incorrect.",
};
code: 'auth/wrong-password',
message: 'Mot de passe incorrect.',
}
}
await user.updateEmail(val);
await usersRef.doc(uid).set({ email: val }, { merge: true });
setTooltip({ type: "success", text: "Email mis à jour" });
goBack();
await user.updateEmail(val)
await usersRef.doc(uid).set({ email: val }, { merge: true })
setTooltip({ type: 'success', text: 'Email mis à jour' })
goBack()
} catch (e) {
console.log("ChangeEmailAddress error", e?.message);
console.log('ChangeEmailAddress error', e?.message)
const msg =
e?.code === "auth/requires-recent-login"
e?.code === 'auth/requires-recent-login'
? "Veuillez vous reconnecter pour changer l'adresse email"
: e?.message || "Mise à jour impossible";
setTooltip({ type: "error", text: msg });
: e?.message || 'Mise à jour impossible'
setTooltip({ type: 'error', text: msg })
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
headerType="NAVIGATE"
@@ -91,17 +88,17 @@ const ChangeEmailAddress = () => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingHorizontal: 20,
paddingVertical: 23,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
}}
// experimentalBlurMethod={
@@ -138,10 +135,10 @@ const ChangeEmailAddress = () => {
</BlurView>
</View>
<GradientButton
title={loading ? "Chargement..." : "Enregistrer les modifications"}
title={loading ? 'Chargement...' : 'Enregistrer les modifications'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onSave}
disabled={
@@ -153,7 +150,7 @@ const ChangeEmailAddress = () => {
}
/>
</Page>
);
};
)
}
export default ChangeEmailAddress;
export default ChangeEmailAddress
+44 -47
View File
@@ -1,50 +1,47 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import { Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import firebase from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import EditInput from "./components/EditInput";
import { BlurView } from 'expo-blur'
import React, { useState } from 'react'
import { Platform, Pressable, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import firebase from '../../config/firebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import EditInput from './components/EditInput'
const ChangePassword = () => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [loading, setLoading] = useState(false)
const [, setTooltip] = useGlobal('_tooltip')
const onSave = async () => {
const oldVal = (oldPassword || "").trim();
const newVal = (newPassword || "").trim();
if (!oldVal || !newVal) return;
const oldVal = (oldPassword || '').trim()
const newVal = (newPassword || '').trim()
if (!oldVal || !newVal) return
try {
setLoading(true);
const user = firebase.auth().currentUser;
if (!user?.email) return;
setLoading(true)
const user = firebase.auth().currentUser
if (!user?.email) return
const credential = firebase.auth.EmailAuthProvider.credential(
user.email,
oldVal
);
await user.reauthenticateWithCredential(credential);
await user.updatePassword(newVal);
setTooltip({ type: "success", text: "Mot de passe mis à jour" });
navigate(Routes.Settings);
const credential = firebase.auth.EmailAuthProvider.credential(user.email, oldVal)
await user.reauthenticateWithCredential(credential)
await user.updatePassword(newVal)
setTooltip({ type: 'success', text: 'Mot de passe mis à jour' })
navigate(Routes.Settings)
} catch (e) {
console.log("ChangePassword error", e?.message);
const msg = e?.message || "Mise à jour impossible";
setTooltip({ type: "error", text: msg });
console.log('ChangePassword error', e?.message)
const msg = e?.message || 'Mise à jour impossible'
setTooltip({ type: 'error', text: msg })
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<Page
headerType="NAVIGATE"
@@ -54,17 +51,17 @@ const ChangePassword = () => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingHorizontal: 20,
paddingVertical: 16,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
gap: 4,
}}
@@ -100,16 +97,16 @@ const ChangePassword = () => {
</BlurView>
</View>
<GradientButton
title={loading ? "Chargement..." : "Enregistrer les modifications"}
title={loading ? 'Chargement...' : 'Enregistrer les modifications'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onSave}
disabled={loading || !oldPassword?.trim() || !newPassword?.trim()}
/>
</Page>
);
};
)
}
export default ChangePassword;
export default ChangePassword
+75 -83
View File
@@ -1,55 +1,52 @@
import { BlurView } from "expo-blur";
import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useState } from "react";
import { Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background } from "../../assets";
import GradientButton from "../../components/GradientButton";
import ProfilePicture from "../../components/ProfilePicture";
import firebase, { serverTimestamp, usersRef } from "../../config/firebase";
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import EditInput from "./components/EditInput";
import { BlurView } from 'expo-blur'
import * as ImagePicker from 'expo-image-picker'
import React, { useEffect, useState } from 'react'
import { Platform, Pressable, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background } from '../../assets'
import GradientButton from '../../components/GradientButton'
import ProfilePicture from '../../components/ProfilePicture'
import firebase, { serverTimestamp, usersRef } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import EditInput from './components/EditInput'
const EditProfile = () => {
const [currentUserData] = useGlobal("currentUserData");
const [, setTooltip] = useGlobal("_tooltip");
const { currentUID } = useUser();
const [pseudo, setPseudo] = useState("");
const [loading, setLoading] = useState(false);
const [updatingPhoto, setUpdatingPhoto] = useState(false);
const [currentUserData] = useGlobal('currentUserData')
const [, setTooltip] = useGlobal('_tooltip')
const { currentUID } = useUser()
const [pseudo, setPseudo] = useState('')
const [loading, setLoading] = useState(false)
const [updatingPhoto, setUpdatingPhoto] = useState(false)
useEffect(() => {
if (currentUserData?.userName && !pseudo) {
setPseudo(currentUserData.userName);
setPseudo(currentUserData.userName)
}
}, [currentUserData?.userName]);
}, [currentUserData?.userName])
const onSave = async () => {
const raw = pseudo || "";
const val = raw.trim();
if (!val) return;
const raw = pseudo || ''
const val = raw.trim()
if (!val) return
try {
setLoading(true);
const uid = firebase.auth().currentUser?.uid;
if (!uid) return;
setLoading(true)
const uid = firebase.auth().currentUser?.uid
if (!uid) return
// Check uniqueness (case-insensitive)
const lower = val.toLowerCase();
const snap = await usersRef
.where("userNameLower", "==", lower)
.limit(1)
.get();
const lower = val.toLowerCase()
const snap = await usersRef.where('userNameLower', '==', lower).limit(1).get()
if (!snap.empty && snap.docs[0].id !== uid) {
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
return;
setTooltip({ text: 'Ce pseudo est déjà pris', type: 'error' })
return
}
await usersRef.doc(uid).set(
@@ -59,43 +56,43 @@ const EditProfile = () => {
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
)
setTooltip({ text: "Pseudo mis à jour", type: "success" });
goBack();
setTooltip({ text: 'Pseudo mis à jour', type: 'success' })
goBack()
} catch (e) {
console.log("EditProfile onSave error", e?.message);
console.log('EditProfile onSave error', e?.message)
setTooltip({
text: e?.message || "Mise à jour impossible",
type: "error",
});
text: e?.message || 'Mise à jour impossible',
type: 'error',
})
} finally {
setLoading(false);
setLoading(false)
}
};
}
const onChangePicture = async () => {
try {
setUpdatingPhoto(true);
const uid = currentUID;
if (!uid) return;
setUpdatingPhoto(true)
const uid = currentUID
if (!uid) return
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 4],
quality: 1,
});
})
const pickedUri = result?.assets?.[0]?.uri || null;
if (!pickedUri) return;
const pickedUri = result?.assets?.[0]?.uri || null
if (!pickedUri) return
const { resultURI = null } = await uploadFileToFirebase({
uri: pickedUri,
path: `users/${uid}/profilePicture.png`,
});
})
if (!resultURI) throw new Error("Téléversement de l'image impossible");
if (!resultURI) throw new Error("Téléversement de l'image impossible")
await usersRef.doc(uid).set(
{
@@ -103,22 +100,22 @@ const EditProfile = () => {
updatedAt: serverTimestamp(),
},
{ merge: true }
);
)
await firebase.auth().currentUser.updateProfile({
photoURL: resultURI,
});
})
setTooltip({ text: "Photo de profil mise à jour", type: "success" });
setTooltip({ text: 'Photo de profil mise à jour', type: 'success' })
} catch (e) {
setTooltip({
text: e?.message || "Erreur changement photo de profil",
type: "error",
});
text: e?.message || 'Erreur changement photo de profil',
type: 'error',
})
} finally {
setUpdatingPhoto(false);
setUpdatingPhoto(false)
}
};
}
return (
<Page
headerType="NAVIGATE"
@@ -127,29 +124,29 @@ const EditProfile = () => {
paddingBottom: gutters * 2,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingVertical: 14,
backgroundColor: Palette.glass,
paddingHorizontal: 20,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
gap: 24,
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Pressable style={{ alignItems: "center" }} onPress={onChangePicture}>
<Pressable style={{ alignItems: 'center' }} onPress={onChangePicture}>
<ProfilePicture
uri={currentUserData?.profilePictureURL || null}
size={108}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
<Text
style={{
@@ -158,34 +155,29 @@ const EditProfile = () => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{updatingPhoto ? "Chargement..." : "Modifier la photo"}
{updatingPhoto ? 'Chargement...' : 'Modifier la photo'}
</Text>
</Pressable>
<EditInput
label="Pseudo"
placeholder="Pseudo"
value={pseudo}
setValue={setPseudo}
/>
<EditInput label="Pseudo" placeholder="Pseudo" value={pseudo} setValue={setPseudo} />
<GradientButton
title={loading ? "Chargement..." : "Enregistrer les modifications"}
title={loading ? 'Chargement...' : 'Enregistrer les modifications'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={onSave}
disabled={
loading ||
updatingPhoto ||
!pseudo?.trim() ||
pseudo?.trim() === (currentUserData?.userName || "")
pseudo?.trim() === (currentUserData?.userName || '')
}
/>
</BlurView>
</View>
</Page>
);
};
)
}
export default EditProfile;
export default EditProfile
+51 -69
View File
@@ -1,25 +1,25 @@
import React, { useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { useUser } from "../../providers/UserDataProvider";
import { usersRef } from "../../config/firebase";
import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId";
import useDataFromRef from "../../hooks/useDataFromRef";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { navigate } from "../../navigation/NavigationService";
import { getArtistDisplayName } from "../../utils/artistName";
import ProfilePicture from "../../components/ProfilePicture";
import React, { useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import Page from '../../layouts/Page'
import { background } from '../../assets'
import { Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { useUser } from '../../providers/UserDataProvider'
import { usersRef } from '../../config/firebase'
import useDataFromArrayId from 'react-native-minuit/src/hooks/useDataFromArrayId'
import useDataFromRef from '../../hooks/useDataFromRef'
import { Routes } from '../../navigation'
import { useRoute } from '@react-navigation/native'
import { navigate } from '../../navigation/NavigationService'
import { getArtistDisplayName } from '../../utils/artistName'
import ProfilePicture from '../../components/ProfilePicture'
const Follows = () => {
const { params } = useRoute();
const initial = params?.selected || "Abonnés"; // "Abonnés" | "Abonnements"
const targetUserId = params?.userId || null;
const [selected, setSelected] = useState(initial);
const { currentUID, currentUserData } = useUser();
const { params } = useRoute()
const initial = params?.selected || 'Abonnés' // "Abonnés" | "Abonnements"
const targetUserId = params?.userId || null
const [selected, setSelected] = useState(initial)
const { currentUID, currentUserData } = useUser()
// Abonnés (followers): docs for IDs in currentUserData.followedBy
// If viewing another user's profile, subscribe to that user's doc
@@ -29,48 +29,39 @@ const Follows = () => {
listener: true,
condition: !!targetUserId,
refreshArray: [targetUserId],
});
})
const followerIds = useMemo(() => {
if (targetUserId) {
return Array.isArray(targetUserDoc?.followedBy)
? targetUserDoc.followedBy
: [];
return Array.isArray(targetUserDoc?.followedBy) ? targetUserDoc.followedBy : []
}
return Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy
: [];
return Array.isArray(currentUserData?.followedBy) ? currentUserData.followedBy : []
}, [
targetUserId,
targetUserDoc?.followedBy?.length || 0,
currentUserData?.followedBy?.length || 0,
]);
const { data: followers = [], loading: followersLoading } =
useDataFromArrayId({
ref: usersRef,
arrayId: followerIds,
condition: followerIds.length > 0,
refreshArray: [followerIds.length],
});
])
const { data: followers = [], loading: followersLoading } = useDataFromArrayId({
ref: usersRef,
arrayId: followerIds,
condition: followerIds.length > 0,
refreshArray: [followerIds.length],
})
// Abonnements (following): users whose followedBy includes currentUID
const { data: following = [], loading: followingLoading } = useDataFromRef({
ref:
targetUserId || currentUID
? usersRef.where(
"followedBy",
"array-contains",
targetUserId || currentUID,
)
? usersRef.where('followedBy', 'array-contains', targetUserId || currentUID)
: null,
simpleRef: false,
listener: true,
condition: !!(targetUserId || currentUID),
refreshArray: [targetUserId || currentUID],
});
})
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<View style={{ flex: 1, alignItems: 'center', paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
@@ -82,10 +73,10 @@ const Follows = () => {
{text}
</Text>
</View>
);
)
const UserRow = ({ user }) => {
const displayName = getArtistDisplayName(user, "Utilisateur");
const displayName = getArtistDisplayName(user, 'Utilisateur')
return (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
@@ -94,7 +85,7 @@ const Follows = () => {
<ProfilePicture
uri={user?.profilePictureURL || null}
size={60}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
<Text
style={{
@@ -106,36 +97,31 @@ const Follows = () => {
{displayName}
</Text>
</Pressable>
);
};
)
}
const list = selected === "Abonnés" ? followers : following;
const loading = selected === "Abonnés" ? followersLoading : followingLoading;
const list = selected === 'Abonnés' ? followers : following
const loading = selected === 'Abonnés' ? followersLoading : followingLoading
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.profileBG}
title={"Abonnés et abonnements"}
title={'Abonnés et abonnements'}
contentContainerStyle={{ paddingBottom: 20 }}
>
<View style={{ gap: 12 }}>
<View style={{ ...Style.containerRow, gap: 6 }}>
{["Abonnés", "Abonnements"].map((item) => (
<Pressable
key={item}
onPress={() => setSelected(item)}
style={{ flex: 1 }}
>
{['Abonnés', 'Abonnements'].map((item) => (
<Pressable key={item} onPress={() => setSelected(item)} style={{ flex: 1 }}>
<View
style={{
height: 30,
borderRadius: 100,
overflow: "hidden",
backgroundColor:
selected === item ? "#FFFFFF22" : Palette.glass,
alignItems: "center",
justifyContent: "center",
overflow: 'hidden',
backgroundColor: selected === item ? '#FFFFFF22' : Palette.glass,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
@@ -155,16 +141,12 @@ const Follows = () => {
{Array.isArray(list) && list.length > 0 ? (
list.map((u) => <UserRow key={u.id} user={u} />)
) : !loading ? (
<EmptyText
text={
selected === "Abonnés" ? "Aucun abonné" : "Aucun abonnement"
}
/>
<EmptyText text={selected === 'Abonnés' ? 'Aucun abonné' : 'Aucun abonnement'} />
) : null}
</View>
</View>
</Page>
);
};
)
}
export default Follows;
export default Follows
+18 -18
View File
@@ -1,18 +1,18 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import { Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import AppCheckbox from "../../components/AppCheckbox";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { BlurView } from 'expo-blur'
import React, { useState } from 'react'
import { Platform, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import AppCheckbox from '../../components/AppCheckbox'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
const LANGUAGE = ["Anglais", "Français", "Chinois", "Japonais", "Coreen"];
const LANGUAGE = ['Anglais', 'Français', 'Chinois', 'Japonais', 'Coreen']
const Language = () => {
const [selected, setSelected] = useState("Français");
const [selected, setSelected] = useState('Français')
return (
<Page
@@ -23,19 +23,19 @@ const Language = () => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingVertical: 10,
paddingHorizontal: 20,
backgroundColor: Palette.glass,
gap: 10,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -63,7 +63,7 @@ const Language = () => {
</BlurView>
</View>
</Page>
);
};
)
}
export default Language;
export default Language
+275 -347
View File
@@ -1,431 +1,394 @@
import { useFocusEffect } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from "react";
import { Alert, Platform, StyleSheet, Text, View } from "react-native";
import BorderGradientButton from "../../components/BorderGradientButton";
import CreditAmount from "../../components/CreditAmount";
import { background } from "../../assets";
import { getFunctionsClient } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation/Routes";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { formatDate, toDate } from "../../utils/dateFormatting";
import { Image as ExpoImage } from "expo-image";
import { isWeb } from "../../hooks/useLayoutType";
import ClubAdvantagesCard from "./components/ClubAdvantagesCard";
import { useFocusEffect } from '@react-navigation/native'
import React, { useCallback, useMemo, useState } from 'react'
import { Alert, Platform, StyleSheet, Text, View } from 'react-native'
import BorderGradientButton from '../../components/BorderGradientButton'
import CreditAmount from '../../components/CreditAmount'
import { background } from '../../assets'
import { getFunctionsClient } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation/Routes'
import { useUserData } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { formatDate, toDate } from '../../utils/dateFormatting'
import { Image as ExpoImage } from 'expo-image'
import { isWeb } from '../../hooks/useLayoutType'
import ClubAdvantagesCard from './components/ClubAdvantagesCard'
const FUNCTIONS_REGION = "europe-west1";
const FUNCTIONS_REGION = 'europe-west1'
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
"trialing",
"active",
"past_due",
"unpaid",
]);
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['trialing', 'active', 'past_due', 'unpaid'])
const STATUS_LABELS = {
trialing: "Période d'essai",
active: "Actif",
past_due: "Paiement en attente",
unpaid: "Impaye",
canceled: "Annulé",
incomplete: "Incomplet",
incomplete_expired: "Expiré",
paused: "En pause",
};
active: 'Actif',
past_due: 'Paiement en attente',
unpaid: 'Impaye',
canceled: 'Annulé',
incomplete: 'Incomplet',
incomplete_expired: 'Expiré',
paused: 'En pause',
}
const PLAN_LABELS = {
starter: "Starter",
pro: "Pro",
premium: "Premium",
};
starter: 'Starter',
pro: 'Pro',
premium: 'Premium',
}
const PERIOD_LABELS = {
monthly: "Mensuel",
annual: "Annuel",
};
monthly: 'Mensuel',
annual: 'Annuel',
}
const PAGE_BACKGROUND_COLOR = "#252438";
const PAGE_BACKGROUND_COLOR = '#252438'
const getStatusColors = (status) => {
switch (status) {
case "trialing":
case "active":
case 'trialing':
case 'active':
return {
text: Palette.green,
background: Palette.transparentGreen,
};
case "past_due":
case "unpaid":
}
case 'past_due':
case 'unpaid':
return {
text: Palette.orange,
background: Palette.transparentOrange,
};
case "canceled":
case "incomplete_expired":
}
case 'canceled':
case 'incomplete_expired':
return {
text: Palette.red,
background: Palette.transparentRed,
};
}
default:
return {
text: Palette.grayMid,
background: Palette.ultraLightWhite,
};
}
}
};
}
const capitalize = (value) => {
if (typeof value !== "string" || !value) {
return null;
if (typeof value !== 'string' || !value) {
return null
}
return value.charAt(0).toUpperCase() + value.slice(1);
};
return value.charAt(0).toUpperCase() + value.slice(1)
}
const SCHEDULE_COMPARISON_HOUR = 2;
const SCHEDULE_COMPARISON_MINUTE = 30;
const SCHEDULE_DISPLAY_HOUR = 3;
const SCHEDULE_DISPLAY_MINUTE = 30;
const SCHEDULE_COMPARISON_HOUR = 2
const SCHEDULE_COMPARISON_MINUTE = 30
const SCHEDULE_DISPLAY_HOUR = 3
const SCHEDULE_DISPLAY_MINUTE = 30
const addMonthsSafe = (date, months = 1) => {
if (!(date instanceof Date) || !Number.isFinite(months)) {
return null;
return null
}
const result = new Date(date.getTime());
const initialDay = result.getDate();
result.setMonth(result.getMonth() + months);
const result = new Date(date.getTime())
const initialDay = result.getDate()
result.setMonth(result.getMonth() + months)
if (result.getDate() !== initialDay) {
result.setDate(0);
result.setDate(0)
}
return result;
};
return result
}
const addDaysSafe = (date, days = 1) => {
if (!(date instanceof Date) || !Number.isFinite(days)) {
return null;
return null
}
const result = new Date(date.getTime());
result.setDate(result.getDate() + days);
return result;
};
const result = new Date(date.getTime())
result.setDate(result.getDate() + days)
return result
}
const alignToScheduleTime = (date) => {
if (!(date instanceof Date)) {
return null;
return null
}
const aligned = new Date(date.getTime());
aligned.setHours(SCHEDULE_DISPLAY_HOUR, SCHEDULE_DISPLAY_MINUTE, 0, 0);
return aligned;
};
const aligned = new Date(date.getTime())
aligned.setHours(SCHEDULE_DISPLAY_HOUR, SCHEDULE_DISPLAY_MINUTE, 0, 0)
return aligned
}
const computeFirstAnnualGrantFromCreation = (creationDate) => {
if (!(creationDate instanceof Date)) {
return null;
return null
}
const cutoff = new Date(creationDate.getTime());
cutoff.setHours(SCHEDULE_COMPARISON_HOUR, SCHEDULE_COMPARISON_MINUTE, 0, 0);
const cutoff = new Date(creationDate.getTime())
cutoff.setHours(SCHEDULE_COMPARISON_HOUR, SCHEDULE_COMPARISON_MINUTE, 0, 0)
let base = null;
let base = null
if (creationDate <= cutoff) {
base = addMonthsSafe(creationDate, 1);
base = addMonthsSafe(creationDate, 1)
} else {
base = addDaysSafe(creationDate, 1);
base = addDaysSafe(creationDate, 1)
}
if (!base) {
return null;
return null
}
return alignToScheduleTime(base);
};
return alignToScheduleTime(base)
}
const ManageSubscription = ({ navigation }) => {
const { currentUserData } = useUserData() || {};
const [isCancelling, setIsCancelling] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const [remoteSubscription, setRemoteSubscription] = useState(null);
const [remoteError, setRemoteError] = useState(null);
const [isFetchingRemote, setIsFetchingRemote] = useState(false);
const [refreshToken, setRefreshToken] = useState(0);
const { currentUserData } = useUserData() || {}
const [isCancelling, setIsCancelling] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const [successMessage, setSuccessMessage] = useState(null)
const [remoteSubscription, setRemoteSubscription] = useState(null)
const [remoteError, setRemoteError] = useState(null)
const [isFetchingRemote, setIsFetchingRemote] = useState(false)
const [refreshToken, setRefreshToken] = useState(0)
const stripeCustomerId = currentUserData?.stripeCustomerId || null;
const stripeCustomerId = currentUserData?.stripeCustomerId || null
const localSubscriptionId =
currentUserData?.stripeSubscription?.id ||
currentUserData?.stripeSubscription?.subscriptionId ||
null;
const localSubscription = currentUserData?.stripeSubscription || null;
null
const localSubscription = currentUserData?.stripeSubscription || null
const triggerRefresh = useCallback(() => {
setRefreshToken((value) => value + 1);
setIsFetchingRemote(true);
}, []);
setRefreshToken((value) => value + 1)
setIsFetchingRemote(true)
}, [])
useFocusEffect(
useCallback(() => {
triggerRefresh();
}, [triggerRefresh]),
);
triggerRefresh()
}, [triggerRefresh])
)
React.useEffect(() => {
let isMounted = true;
let isMounted = true
const run = async () => {
const hasLookupContext =
Boolean(stripeCustomerId) || Boolean(localSubscriptionId);
const hasLookupContext = Boolean(stripeCustomerId) || Boolean(localSubscriptionId)
if (!hasLookupContext) {
if (isMounted) {
setRemoteSubscription(null);
setRemoteError(null);
setIsFetchingRemote(false);
setRemoteSubscription(null)
setRemoteError(null)
setIsFetchingRemote(false)
}
return;
return
}
setIsFetchingRemote(true);
setRemoteError(null);
setIsFetchingRemote(true)
setRemoteError(null)
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-getActiveSubscription",
);
const { data } = await callable();
'subscription-getActiveSubscription'
)
const { data } = await callable()
if (!isMounted) {
return;
return
}
setRemoteSubscription(data?.subscription || null);
setRemoteSubscription(data?.subscription || null)
} catch (error) {
console.warn(
"[ManageSubscription] fetch subscription error",
error?.message || error,
);
console.warn('[ManageSubscription] fetch subscription error', error?.message || error)
if (isMounted) {
const message =
error?.message ||
"Impossible de mettre à jour les informations d'abonnement.";
setRemoteError(message);
error?.message || "Impossible de mettre à jour les informations d'abonnement."
setRemoteError(message)
}
} finally {
if (isMounted) {
setIsFetchingRemote(false);
setIsFetchingRemote(false)
}
}
};
}
run();
run()
return () => {
isMounted = false;
};
}, [stripeCustomerId, localSubscriptionId, refreshToken]);
isMounted = false
}
}, [stripeCustomerId, localSubscriptionId, refreshToken])
const subscriptionInfo = useMemo(() => {
const subscriptionSources = [remoteSubscription, localSubscription].filter(
Boolean,
);
const subscriptionSources = [remoteSubscription, localSubscription].filter(Boolean)
const pickString = (value) => {
if (typeof value !== "string") {
return null;
if (typeof value !== 'string') {
return null
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
};
const trimmed = value.trim()
return trimmed ? trimmed : null
}
const pickFromSources = (resolver) => {
for (const source of subscriptionSources) {
if (!source) {
continue;
continue
}
const value = resolver(source);
const value = resolver(source)
if (value !== undefined && value !== null) {
return value;
return value
}
}
return null;
};
return null
}
const pickDateFromSources = (...resolvers) => {
for (const source of subscriptionSources) {
if (!source) {
continue;
continue
}
for (const resolveValue of resolvers) {
const date = toDate(resolveValue(source));
const date = toDate(resolveValue(source))
if (date) {
return date;
return date
}
}
}
return null;
};
return null
}
const statusSource =
pickString(currentUserData?.stripeSubscriptionStatus) ||
pickString(pickFromSources((source) => source?.status)) ||
pickString(
pickFromSources((source) => source?.stripeSubscriptionStatus),
) ||
pickString(pickFromSources((source) => source?.stripeSubscriptionStatus)) ||
pickString(pickFromSources((source) => source?.metadata?.status)) ||
null;
null
const status = statusSource ? statusSource.toLowerCase() : null;
const status = statusSource ? statusSource.toLowerCase() : null
const cancelAtPeriodEnd = subscriptionSources.some(
(source) =>
source?.cancelAtPeriodEnd === true ||
source?.cancel_at_period_end === true,
);
(source) => source?.cancelAtPeriodEnd === true || source?.cancel_at_period_end === true
)
const resolveLevelSource = () => {
const candidates = [
pickString(pickFromSources((source) => source?.level)),
pickString(currentUserData?.premiumLevel),
pickString(pickFromSources((source) => source?.metadata?.level)),
pickString(
pickFromSources((source) => source?.metadata?.subscriptionLevel),
),
];
return candidates.find(Boolean) || null;
};
pickString(pickFromSources((source) => source?.metadata?.subscriptionLevel)),
]
return candidates.find(Boolean) || null
}
const resolvePeriodSource = () => {
const candidates = [
pickString(pickFromSources((source) => source?.billingPeriod)),
pickString(currentUserData?.premiumBillingPeriod),
pickString(
pickFromSources((source) => source?.metadata?.billingPeriod),
),
pickString(
pickFromSources(
(source) => source?.metadata?.subscriptionBillingPeriod,
),
),
];
return candidates.find(Boolean) || null;
};
pickString(pickFromSources((source) => source?.metadata?.billingPeriod)),
pickString(pickFromSources((source) => source?.metadata?.subscriptionBillingPeriod)),
]
return candidates.find(Boolean) || null
}
const levelSource = resolveLevelSource();
const periodSource = resolvePeriodSource();
const levelSource = resolveLevelSource()
const periodSource = resolvePeriodSource()
const level = levelSource ? levelSource.toLowerCase() : null;
const billingPeriod = periodSource ? periodSource.toLowerCase() : null;
const level = levelSource ? levelSource.toLowerCase() : null
const billingPeriod = periodSource ? periodSource.toLowerCase() : null
const isAnnual = billingPeriod === "annual";
const isAnnual = billingPeriod === 'annual'
const planLabelParts = [];
const planLabelParts = []
if (PLAN_LABELS[level]) {
planLabelParts.push(PLAN_LABELS[level]);
planLabelParts.push(PLAN_LABELS[level])
} else if (level) {
planLabelParts.push(capitalize(level));
planLabelParts.push(capitalize(level))
}
if (PERIOD_LABELS[billingPeriod]) {
planLabelParts.push(PERIOD_LABELS[billingPeriod]);
planLabelParts.push(PERIOD_LABELS[billingPeriod])
} else if (billingPeriod) {
planLabelParts.push(capitalize(billingPeriod));
planLabelParts.push(capitalize(billingPeriod))
}
const planLabel =
planLabelParts.length > 0
? planLabelParts.join(" · ")
: "Abonnement Musicland";
planLabelParts.length > 0 ? planLabelParts.join(' · ') : 'Abonnement Musicland'
const currentPeriodEndDate = pickDateFromSources(
(source) => source?.currentPeriodEnd,
(source) => source?.current_period_end,
);
(source) => source?.current_period_end
)
const createdAtDate = pickDateFromSources(
(source) => source?.created,
(source) => source?.createdAt,
(source) => source?.created_at,
);
(source) => source?.created_at
)
const statusLabelBase =
STATUS_LABELS[status] ||
(status ? status.replace(/_/g, " ").toLowerCase() : null);
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null;
const statusColors = getStatusColors(status);
STATUS_LABELS[status] || (status ? status.replace(/_/g, ' ').toLowerCase() : null)
const statusLabel = statusLabelBase ? capitalize(statusLabelBase) : null
const statusColors = getStatusColors(status)
const subscriptionId =
pickString(pickFromSources((source) => source?.id)) ||
pickString(pickFromSources((source) => source?.subscriptionId)) ||
pickString(currentUserData?.stripeSubscription?.subscriptionId) ||
null;
null
const hasAnySubscription = Boolean(subscriptionId);
const hasActiveSubscription =
hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status);
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd;
const hasAnySubscription = Boolean(subscriptionId)
const hasActiveSubscription = hasAnySubscription && ACTIVE_SUBSCRIPTION_STATUSES.has(status)
const canCancel = hasActiveSubscription && !cancelAtPeriodEnd
const periodEndLabel = currentPeriodEndDate
? formatDate(currentPeriodEndDate)
: "—";
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null;
const periodEndLabel = currentPeriodEndDate ? formatDate(currentPeriodEndDate) : '—'
const createdAtLabel = createdAtDate ? formatDate(createdAtDate) : null
let coinsPerMonth = pickFromSources((source) => source?.coinsPerMonth);
let coinsPerMonth = pickFromSources((source) => source?.coinsPerMonth)
if (
coinsPerMonth === null &&
typeof currentUserData?.subscriptionCoinsPerMonth === "number"
) {
const userCoins = currentUserData.subscriptionCoinsPerMonth;
coinsPerMonth = Number.isFinite(userCoins) ? userCoins : null;
if (coinsPerMonth === null && typeof currentUserData?.subscriptionCoinsPerMonth === 'number') {
const userCoins = currentUserData.subscriptionCoinsPerMonth
coinsPerMonth = Number.isFinite(userCoins) ? userCoins : null
}
const normalizedCoins =
typeof coinsPerMonth === "number" && Number.isFinite(coinsPerMonth)
typeof coinsPerMonth === 'number' && Number.isFinite(coinsPerMonth)
? Math.round(coinsPerMonth)
: null;
: null
const grantStrategy =
typeof currentUserData?.subscriptionGrantStrategy === "string"
typeof currentUserData?.subscriptionGrantStrategy === 'string'
? currentUserData.subscriptionGrantStrategy
: null;
const isUpfrontGrant = grantStrategy === "upfront";
: null
const isUpfrontGrant = grantStrategy === 'upfront'
const nextGrantTimestamp = isUpfrontGrant
? null
: currentUserData?.subscriptionNextGrantAt ||
currentUserData?.subscriptionGrantNextAt ||
null;
const nextGrantRawDate = toDate(nextGrantTimestamp);
const now = new Date();
: currentUserData?.subscriptionNextGrantAt || currentUserData?.subscriptionGrantNextAt || null
const nextGrantRawDate = toDate(nextGrantTimestamp)
const now = new Date()
const alignedStoredNextGrant = alignToScheduleTime(nextGrantRawDate);
const alignedStoredNextGrant = alignToScheduleTime(nextGrantRawDate)
const futureStoredNextGrant =
alignedStoredNextGrant &&
alignedStoredNextGrant.getTime() >= now.getTime()
alignedStoredNextGrant && alignedStoredNextGrant.getTime() >= now.getTime()
? alignedStoredNextGrant
: null;
: null
const fallbackInitialGrant =
!isUpfrontGrant && isAnnual && createdAtDate
? computeFirstAnnualGrantFromCreation(createdAtDate)
: null;
: null
const futureFallbackGrant =
fallbackInitialGrant && fallbackInitialGrant.getTime() >= now.getTime()
? fallbackInitialGrant
: null;
: null
let resolvedNextGrantDate =
futureStoredNextGrant || futureFallbackGrant || null;
let resolvedNextGrantDate = futureStoredNextGrant || futureFallbackGrant || null
if (futureStoredNextGrant && futureFallbackGrant) {
resolvedNextGrantDate =
futureFallbackGrant.getTime() < futureStoredNextGrant.getTime()
? futureFallbackGrant
: futureStoredNextGrant;
: futureStoredNextGrant
}
const nextGrantLabel =
resolvedNextGrantDate && isAnnual && !isUpfrontGrant
? formatDate(resolvedNextGrantDate)
: null;
: null
const helperMessages = [];
const helperMessages = []
if (!hasActiveSubscription && hasAnySubscription) {
helperMessages.push(
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment.",
);
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment."
)
}
const helperMessage = helperMessages.join("\n");
const helperMessage = helperMessages.join('\n')
return {
hasAnySubscription,
@@ -436,8 +399,7 @@ const ManageSubscription = ({ navigation }) => {
statusLabel,
statusColors,
planLabel,
billingPeriodLabel:
PERIOD_LABELS[billingPeriod] || capitalize(billingPeriod),
billingPeriodLabel: PERIOD_LABELS[billingPeriod] || capitalize(billingPeriod),
periodEndLabel,
createdAtLabel,
helperMessage: helperMessage || null,
@@ -447,105 +409,97 @@ const ManageSubscription = ({ navigation }) => {
isAnnual,
nextGrantDate: resolvedNextGrantDate,
nextGrantLabel,
};
}, [currentUserData, localSubscription, remoteSubscription]);
}
}, [currentUserData, localSubscription, remoteSubscription])
const handleOpenPlans = useCallback(() => {
const params =
subscriptionInfo.level && typeof subscriptionInfo.level === "string"
subscriptionInfo.level && typeof subscriptionInfo.level === 'string'
? { pack: subscriptionInfo.level }
: undefined;
navigation.navigate(Routes.Payments, params);
}, [navigation, subscriptionInfo.level]);
: undefined
navigation.navigate(Routes.Payments, params)
}, [navigation, subscriptionInfo.level])
const performCancellation = useCallback(async () => {
setIsCancelling(true);
setErrorMessage(null);
setSuccessMessage(null);
setIsCancelling(true)
setErrorMessage(null)
setSuccessMessage(null)
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-cancelActiveSubscription",
);
'subscription-cancelActiveSubscription'
)
const payload = subscriptionInfo.subscriptionId
? { subscriptionId: subscriptionInfo.subscriptionId }
: {};
const { data } = await callable(payload);
: {}
const { data } = await callable(payload)
if (data?.alreadyCanceled) {
setSuccessMessage(
"Ton abonnement est déjà en cours d'annulation. L'accès premium restera actif jusqu'à la fin de la période en cours.",
);
"Ton abonnement est déjà en cours d'annulation. L'accès premium restera actif jusqu'à la fin de la période en cours."
)
} else if (data?.cancelAtPeriodEnd) {
setSuccessMessage(
"Ton abonnement sera résilié à la fin de la période en cours.",
);
setSuccessMessage('Ton abonnement sera résilié à la fin de la période en cours.')
} else {
setSuccessMessage(
"La demande d'annulation a été prise en compte. Vérifie ton abonnement dans quelques instants.",
);
"La demande d'annulation a été prise en compte. Vérifie ton abonnement dans quelques instants."
)
}
triggerRefresh();
triggerRefresh()
} catch (error) {
console.warn(
"[ManageSubscription] cancel subscription error",
error?.message || error,
);
const message =
error?.message || "Impossible d'annuler l'abonnement pour le moment.";
setErrorMessage(message);
console.warn('[ManageSubscription] cancel subscription error', error?.message || error)
const message = error?.message || "Impossible d'annuler l'abonnement pour le moment."
setErrorMessage(message)
} finally {
setIsCancelling(false);
setIsCancelling(false)
}
}, [subscriptionInfo.subscriptionId, triggerRefresh]);
}, [subscriptionInfo.subscriptionId, triggerRefresh])
const handleCancel = useCallback(() => {
if (!subscriptionInfo.canCancel || isCancelling) {
return;
return
}
const confirm = () => {
performCancellation();
};
performCancellation()
}
if (Platform.OS === "web" && typeof window !== "undefined") {
if (Platform.OS === 'web' && typeof window !== 'undefined') {
const confirmed = window.confirm(
"Confirmer l'annulation ? Ton accès premium restera actif jusqu'à la fin de la période en cours.",
);
"Confirmer l'annulation ? Ton accès premium restera actif jusqu'à la fin de la période en cours."
)
if (confirmed) {
confirm();
confirm()
}
return;
return
}
Alert.alert(
"Confirmer l'annulation",
"Ton accès premium restera actif jusqu'à la fin de la période en cours.",
[
{ text: "Conserver mon abonnement", style: "cancel" },
{ text: 'Conserver mon abonnement', style: 'cancel' },
{
text: "Annuler l'abonnement",
style: "destructive",
style: 'destructive',
onPress: confirm,
},
],
);
}, [isCancelling, performCancellation, subscriptionInfo.canCancel]);
]
)
}, [isCancelling, performCancellation, subscriptionInfo.canCancel])
const cancelButtonTitle = subscriptionInfo.cancelAtPeriodEnd
? "Annulation programmée"
? 'Annulation programmée'
: isCancelling
? "Annulation..."
: "Annuler l'abonnement";
? 'Annulation...'
: "Annuler l'abonnement"
const cancelTitleColor = subscriptionInfo.cancelAtPeriodEnd
? Palette.grayMid
: Palette.red;
const cancelTitleColor = subscriptionInfo.cancelAtPeriodEnd ? Palette.grayMid : Palette.red
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120;
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80;
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80
return (
<Page
@@ -562,15 +516,15 @@ const ManageSubscription = ({ navigation }) => {
width: HOME_BACKGROUND_STYLE_WIDTH,
height: HOME_BACKGROUND_STYLE_HEIGHT,
borderRadius: 22,
overflow: "hidden",
position: "absolute",
top: isWeb ? "45%" : "50%",
left: "50%",
overflow: 'hidden',
position: 'absolute',
top: isWeb ? '45%' : '50%',
left: '50%',
transform: [
{ translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
],
pointerEvents: "none",
pointerEvents: 'none',
zIndex: 0,
}}
/>
@@ -587,16 +541,12 @@ const ManageSubscription = ({ navigation }) => {
style={[
styles.statusBadge,
{
backgroundColor:
subscriptionInfo.statusColors.background,
backgroundColor: subscriptionInfo.statusColors.background,
},
]}
>
<Text
style={[
styles.statusText,
{ color: subscriptionInfo.statusColors.text },
]}
style={[styles.statusText, { color: subscriptionInfo.statusColors.text }]}
>
{subscriptionInfo.statusLabel}
</Text>
@@ -608,14 +558,12 @@ const ManageSubscription = ({ navigation }) => {
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Cycle de facturation</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.billingPeriodLabel || "—"}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.billingPeriodLabel || '—'}</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Crédits mensuels</Text>
{typeof subscriptionInfo.coinsPerMonth === "number" ? (
{typeof subscriptionInfo.coinsPerMonth === 'number' ? (
<CreditAmount
value={subscriptionInfo.coinsPerMonth}
textStyle={styles.detailValue}
@@ -629,29 +577,21 @@ const ManageSubscription = ({ navigation }) => {
{subscriptionInfo.isAnnual && subscriptionInfo.nextGrantLabel ? (
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Prochain versement</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.nextGrantLabel}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.nextGrantLabel}</Text>
</View>
) : null}
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>
{subscriptionInfo.cancelAtPeriodEnd
? "Fin d'accès"
: "Prochain renouvellement"}
</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.periodEndLabel}
{subscriptionInfo.cancelAtPeriodEnd ? "Fin d'accès" : 'Prochain renouvellement'}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.periodEndLabel}</Text>
</View>
{subscriptionInfo.createdAtLabel ? (
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Abonné depuis</Text>
<Text style={styles.detailValue}>
{subscriptionInfo.createdAtLabel}
</Text>
<Text style={styles.detailValue}>{subscriptionInfo.createdAtLabel}</Text>
</View>
) : null}
@@ -662,16 +602,11 @@ const ManageSubscription = ({ navigation }) => {
) : null}
{subscriptionInfo.helperMessage ? (
<Text style={styles.helperText}>
{subscriptionInfo.helperMessage}
</Text>
<Text style={styles.helperText}>{subscriptionInfo.helperMessage}</Text>
) : null}
{subscriptionInfo.isAnnual &&
typeof subscriptionInfo.coinsPerMonth === "number" ? (
{subscriptionInfo.isAnnual && typeof subscriptionInfo.coinsPerMonth === 'number' ? (
<View style={styles.helperInlineRow}>
<Text style={styles.helperText}>
Tes pièces sont versées chaque mois (
</Text>
<Text style={styles.helperText}>Tes pièces sont versées chaque mois (</Text>
<CreditAmount
value={subscriptionInfo.coinsPerMonth}
textStyle={styles.helperText}
@@ -682,15 +617,9 @@ const ManageSubscription = ({ navigation }) => {
) : null}
</View>
{successMessage ? (
<Text style={styles.successMessage}>{successMessage}</Text>
) : null}
{errorMessage ? (
<Text style={styles.errorMessage}>{errorMessage}</Text>
) : null}
{remoteError ? (
<Text style={styles.errorMessage}>{remoteError}</Text>
) : null}
{successMessage ? <Text style={styles.successMessage}>{successMessage}</Text> : null}
{errorMessage ? <Text style={styles.errorMessage}>{errorMessage}</Text> : null}
{remoteError ? <Text style={styles.errorMessage}>{remoteError}</Text> : null}
<BorderGradientButton
title="Changer d'abonnement"
@@ -716,8 +645,7 @@ const ManageSubscription = ({ navigation }) => {
<View style={styles.card}>
<Text style={styles.cardTitle}>Aucun abonnement actif</Text>
<Text style={styles.infoText}>
Souscris à l’une de nos offres pour profiter des fonctionnalités
premium de Musicland.
Souscris à l’une de nos offres pour profiter des fonctionnalités premium de Musicland.
</Text>
<BorderGradientButton
title="Découvrir les offres"
@@ -729,8 +657,8 @@ const ManageSubscription = ({ navigation }) => {
{/*<ClubAdvantagesCard isDev style={styles.clubCardSpacing} />*/}
</View>
</Page>
);
};
)
}
const styles = StyleSheet.create({
container: {
@@ -743,9 +671,9 @@ const styles = StyleSheet.create({
card: {
padding: gutters * 1.5,
borderRadius: 24,
backgroundColor: "rgba(255, 255, 255, 0.04)",
backgroundColor: 'rgba(255, 255, 255, 0.04)',
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.08)",
borderColor: 'rgba(255, 255, 255, 0.08)',
gap: gutters * 0.75,
},
cardTitle: {
@@ -754,9 +682,9 @@ const styles = StyleSheet.create({
color: Palette.white,
},
detailRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: gutters,
},
detailLabel: {
@@ -770,7 +698,7 @@ const styles = StyleSheet.create({
fontSize: 15,
color: Palette.white,
flexShrink: 0,
textAlign: "right",
textAlign: 'right',
},
statusBadge: {
borderRadius: 999,
@@ -789,9 +717,9 @@ const styles = StyleSheet.create({
color: Palette.grayMid,
},
helperInlineRow: {
flexDirection: "row",
alignItems: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 4,
},
successMessage: {
@@ -822,6 +750,6 @@ const styles = StyleSheet.create({
clubCardSpacing: {
marginTop: gutters,
},
});
})
export default ManageSubscription;
export default ManageSubscription
+41 -42
View File
@@ -1,56 +1,56 @@
/* eslint-disable react/display-name */
import { BlurView } from "expo-blur";
import { useState } from "react";
import { Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import React, { useEffect, useGlobal } from "reactn";
import { background } from "../../assets";
import Switch from "../../components/Switch";
import firebase, { usersRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { gutters, Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { BlurView } from 'expo-blur'
import { useState } from 'react'
import { Platform, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import React, { useEffect, useGlobal } from 'reactn'
import { background } from '../../assets'
import Switch from '../../components/Switch'
import firebase, { usersRef } from '../../config/firebase'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { gutters, Palette, Style } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
export default (props) => {
const [currentUID] = useGlobal("currentUID");
const [currentUserData] = useGlobal("currentUserData");
const [, setTooltip] = useGlobal("_tooltip");
const [currentUID] = useGlobal('currentUID')
const [currentUserData] = useGlobal('currentUserData')
const [, setTooltip] = useGlobal('_tooltip')
const [isPushSwitch, setIsPushSwitch] = useState(false);
const [isEmailSwitch, setIsEmailSwitch] = useState(false);
const [isPushSwitch, setIsPushSwitch] = useState(false)
const [isEmailSwitch, setIsEmailSwitch] = useState(false)
useEffect(() => {
setIsPushSwitch(!!currentUserData?.notifications);
}, [currentUserData?.notifications]);
setIsPushSwitch(!!currentUserData?.notifications)
}, [currentUserData?.notifications])
useEffect(() => {
setIsEmailSwitch(!!currentUserData?.emailNotifications);
}, [currentUserData?.emailNotifications]);
setIsEmailSwitch(!!currentUserData?.emailNotifications)
}, [currentUserData?.emailNotifications])
const updateNotificationPreference = async (next, field, setState) => {
try {
setState(next);
const uid = currentUID || firebase.auth().currentUser?.uid;
setState(next)
const uid = currentUID || firebase.auth().currentUser?.uid
if (!uid) {
throw new Error("Utilisateur non connecté");
throw new Error('Utilisateur non connecté')
}
await usersRef.doc(uid).set({ [field]: next }, { merge: true });
setTooltip({ type: "success", text: "Préférence enregistrée" });
await usersRef.doc(uid).set({ [field]: next }, { merge: true })
setTooltip({ type: 'success', text: 'Préférence enregistrée' })
} catch (e) {
setState(!next);
setState(!next)
setTooltip({
type: "error",
text: e?.message || "Enregistrement impossible",
});
type: 'error',
text: e?.message || 'Enregistrement impossible',
})
}
};
}
const onTogglePush = (next) =>
updateNotificationPreference(next, "notifications", setIsPushSwitch);
updateNotificationPreference(next, 'notifications', setIsPushSwitch)
const onToggleEmail = (next) =>
updateNotificationPreference(next, "emailNotifications", setIsEmailSwitch);
updateNotificationPreference(next, 'emailNotifications', setIsEmailSwitch)
return (
<Page
@@ -61,18 +61,18 @@ export default (props) => {
paddingBottom: gutters * 4,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
gap: 12,
paddingVertical: 15,
paddingHorizontal: 20,
borderRadius: 20,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
}}
// experimentalBlurMethod={
@@ -98,8 +98,8 @@ export default (props) => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Nous t’encourageons à activer les notifications pour découvrir les
derniers classements, les nouveaux sons et les meilleurs playbacks.
Nous t’encourageons à activer les notifications pour découvrir les derniers classements,
les nouveaux sons et les meilleurs playbacks.
</Text>
<View style={Style.containerSpaceBetween}>
<Text
@@ -120,11 +120,10 @@ export default (props) => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Reste informé en recevant nos nouveautés et rappels directement dans
ta boîte mail.
Reste informé en recevant nos nouveautés et rappels directement dans ta boîte mail.
</Text>
</BlurView>
</View>
</Page>
);
};
)
}
+112 -141
View File
@@ -1,210 +1,181 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { ActivityIndicator, FlatList, StyleSheet, Text, View } from 'react-native'
import firebase from "../../config/firebase";
import { background } from "../../assets";
import Page from "../../layouts/Page";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreditAmount from "../../components/CreditAmount";
import firebase from '../../config/firebase'
import { background } from '../../assets'
import Page from '../../layouts/Page'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import CreditAmount from '../../components/CreditAmount'
const ORDER_TYPE_LABELS = {
GIFT: "Crédit offert",
COINS: "Achat de coins",
SONG: "Génération de musique",
SUBSCRIPTION: "Abonnement",
};
GIFT: 'Crédit offert',
COINS: 'Achat de coins',
SONG: 'Génération de musique',
SUBSCRIPTION: 'Abonnement',
}
const formatDateParts = (date) => {
if (!date) {
return {
dateLabel: "En attente de confirmation",
timeLabel: "",
};
dateLabel: 'En attente de confirmation',
timeLabel: '',
}
}
try {
const dateLabel = date.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
year: "numeric",
});
const timeLabel = date.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
});
return { dateLabel, timeLabel };
const dateLabel = date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: 'short',
year: 'numeric',
})
const timeLabel = date.toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit',
})
return { dateLabel, timeLabel }
} catch (_error) {
return {
dateLabel: date.toString(),
timeLabel: "",
};
timeLabel: '',
}
}
};
}
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || "Opération";
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || 'Opération'
const shortenIdentifier = (value, visible = 6) => {
if (typeof value !== "string") return null;
if (value.length <= visible + 2) return value;
return `${value.slice(0, visible)}…`;
};
if (typeof value !== 'string') return null
if (value.length <= visible + 2) return value
return `${value.slice(0, visible)}…`
}
const OrderHistory = () => {
const { currentUID } = useUser() || {};
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { currentUID } = useUser() || {}
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
if (!currentUID) {
setOrders([]);
setLoading(false);
return;
setOrders([])
setLoading(false)
return
}
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const unsubscribe = firebase
.firestore()
.collection("orders")
.where("userId", "==", currentUID)
.orderBy("createdAt", "desc")
.collection('orders')
.where('userId', '==', currentUID)
.orderBy('createdAt', 'desc')
.onSnapshot(
(snapshot) => {
const nextOrders = snapshot.docs.map((doc) => {
const rawData = doc.data() || {};
const rawData = doc.data() || {}
const createdAt =
typeof rawData.createdAt?.toDate === "function"
? rawData.createdAt.toDate()
: null;
typeof rawData.createdAt?.toDate === 'function' ? rawData.createdAt.toDate() : null
return {
id: doc.id,
...rawData,
createdAt,
};
});
}
})
setOrders(nextOrders);
setLoading(false);
setOrders(nextOrders)
setLoading(false)
},
(firestoreError) => {
console.warn(
"[OrderHistory] Unable to load orders",
firestoreError?.message || firestoreError,
);
setError(
firestoreError?.message ||
"Impossible de récupérer l'historique des commandes.",
);
setLoading(false);
},
);
'[OrderHistory] Unable to load orders',
firestoreError?.message || firestoreError
)
setError(firestoreError?.message || "Impossible de récupérer l'historique des commandes.")
setLoading(false)
}
)
return () => {
if (typeof unsubscribe === "function") {
unsubscribe();
if (typeof unsubscribe === 'function') {
unsubscribe()
}
};
}, [currentUID]);
}
}, [currentUID])
const renderOrder = useCallback(({ item }) => {
const amountValue =
typeof item.amount === "number" && Number.isFinite(item.amount)
? item.amount
: 0;
const isPositive = amountValue > 0;
typeof item.amount === 'number' && Number.isFinite(item.amount) ? item.amount : 0
const isPositive = amountValue > 0
const amountTextStyle = [
styles.orderAmount,
isPositive ? styles.amountPositive : styles.amountNegative,
];
]
const { dateLabel } = formatDateParts(item.createdAt);
const { dateLabel } = formatDateParts(item.createdAt)
let reasonLabel = mapOrderType(item.type);
let reasonLabel = mapOrderType(item.type)
if (item.type === "GIFT") {
const reason = item.metadata?.reason;
if (reason === "WELCOME_BONUS") {
reasonLabel = "Crédit de bienvenue";
} else if (typeof reason === "string" && reason.trim()) {
reasonLabel = reason.trim();
if (item.type === 'GIFT') {
const reason = item.metadata?.reason
if (reason === 'WELCOME_BONUS') {
reasonLabel = 'Crédit de bienvenue'
} else if (typeof reason === 'string' && reason.trim()) {
reasonLabel = reason.trim()
}
} else if (item.type === "SONG") {
reasonLabel = "Génération de musique";
} else if (item.type === "COINS") {
} else if (item.type === 'SONG') {
reasonLabel = 'Génération de musique'
} else if (item.type === 'COINS') {
if (item.metadata?.coinPackKey) {
const shortenedPack = shortenIdentifier(
item.metadata.coinPackKey,
10,
);
reasonLabel = `Pack ${shortenedPack || item.metadata.coinPackKey}`;
} else if (
typeof item.metadata?.source === "string" &&
item.metadata.source.trim()
) {
const source = item.metadata.source.trim();
reasonLabel =
source === "STRIPE_CHECKOUT" ? "Recharge Stripe" : source;
const shortenedPack = shortenIdentifier(item.metadata.coinPackKey, 10)
reasonLabel = `Pack ${shortenedPack || item.metadata.coinPackKey}`
} else if (typeof item.metadata?.source === 'string' && item.metadata.source.trim()) {
const source = item.metadata.source.trim()
reasonLabel = source === 'STRIPE_CHECKOUT' ? 'Recharge Stripe' : source
} else {
reasonLabel = "Rechargement de coins";
reasonLabel = 'Rechargement de coins'
}
} else if (item.type === "SUBSCRIPTION") {
} else if (item.type === 'SUBSCRIPTION') {
const rawPeriod =
typeof item.metadata?.billingPeriod === "string"
typeof item.metadata?.billingPeriod === 'string'
? item.metadata.billingPeriod.toLowerCase()
: null;
: null
reasonLabel =
rawPeriod === "annual"
? "Abonnement annuel"
: rawPeriod === "monthly"
? "Abonnement mensuel"
: "Abonnement";
rawPeriod === 'annual'
? 'Abonnement annuel'
: rawPeriod === 'monthly'
? 'Abonnement mensuel'
: 'Abonnement'
}
return (
<View style={styles.orderCard}>
<CreditAmount
value={amountValue}
showPlus
textStyle={amountTextStyle}
iconSize={18}
/>
<CreditAmount value={amountValue} showPlus textStyle={amountTextStyle} iconSize={18} />
<Text style={styles.orderReason}>{reasonLabel}</Text>
<Text style={styles.orderTimestamp}>{dateLabel}</Text>
</View>
);
}, []);
)
}, [])
const keyExtractor = useCallback((item) => item.id, []);
const keyExtractor = useCallback((item) => item.id, [])
const listEmptyComponent = useMemo(() => {
if (loading) {
return null;
return null
}
if (!currentUID) {
return (
<Text style={styles.emptyText}>
Connecte-toi pour consulter l'historique de tes commandes.
</Text>
);
)
}
if (error) {
return <Text style={styles.errorText}>{error}</Text>;
return <Text style={styles.errorText}>{error}</Text>
}
return (
<Text style={styles.emptyText}>
Aucune opération de coins pour le moment.
</Text>
);
}, [loading, currentUID, error]);
return <Text style={styles.emptyText}>Aucune opération de coins pour le moment.</Text>
}, [loading, currentUID, error])
return (
<Page
@@ -213,7 +184,7 @@ const OrderHistory = () => {
backgroundImg={background.profileBG}
contentContainerStyle={{ paddingBottom: gutters * 2 }}
containerStyle={{
backgroundColor: "rgba(0,0,0,0.4)",
backgroundColor: 'rgba(0,0,0,0.4)',
}}
>
<View style={styles.container}>
@@ -236,8 +207,8 @@ const OrderHistory = () => {
)}
</View>
</Page>
);
};
)
}
const styles = StyleSheet.create({
container: {
@@ -247,22 +218,22 @@ const styles = StyleSheet.create({
},
loaderContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
listContent: {
paddingBottom: gutters * 2,
},
emptyListContent: {
flexGrow: 1,
justifyContent: "center",
justifyContent: 'center',
},
orderCard: {
padding: 16,
borderRadius: 16,
backgroundColor: Palette.ultraLightWhite,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.08)",
borderColor: 'rgba(255, 255, 255, 0.08)',
gap: 6,
},
orderAmount: {
@@ -286,17 +257,17 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterMedium,
},
emptyText: {
textAlign: "center",
textAlign: 'center',
color: Palette.grayMid,
fontSize: 15,
fontFamily: FONT_FAMILY.InterRegular,
},
errorText: {
textAlign: "center",
textAlign: 'center',
color: Palette.red,
fontSize: 15,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
export default OrderHistory;
export default OrderHistory
+192 -242
View File
@@ -1,7 +1,7 @@
import { useRoute } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useMemo, useState } from "react";
import { useRoute } from '@react-navigation/native'
import { BlurView } from 'expo-blur'
import * as ImagePicker from 'expo-image-picker'
import React, { useEffect, useMemo, useState } from 'react'
import {
ActivityIndicator,
FlatList,
@@ -11,42 +11,38 @@ import {
StyleSheet,
Text,
View,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { Feather } from "@expo/vector-icons";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, icons } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import MoreMenu from "../../components/MoreMenu";
import MobileCoinBadge from "../../components/MobileCoinBadge";
import PressableScale from "../../components/PressableScale";
import ProfilePicture from "../../components/ProfilePicture";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import firebase, {
projectsRef,
serverTimestamp,
usersRef,
} from "../../config/firebase";
import loaderMessages from "../../config/loaderMessages";
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import useLayoutType from "../../hooks/useLayoutType";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate, push, goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { gutters, size } from "../../styles/Style";
import { getArtistDisplayName } from "../../utils/artistName";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import MusicCard from "../Library/components/MusicCard";
} from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { Feather } from '@expo/vector-icons'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { background, icons } from '../../assets'
import BorderGradient from '../../components/BorderGradient/BorderGradient'
import MoreMenu from '../../components/MoreMenu'
import MobileCoinBadge from '../../components/MobileCoinBadge'
import PressableScale from '../../components/PressableScale'
import ProfilePicture from '../../components/ProfilePicture'
import ShareBtn from '../../components/ShareBtn/ShareBtn'
import firebase, { projectsRef, serverTimestamp, usersRef } from '../../config/firebase'
import loaderMessages from '../../config/loaderMessages'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import useLayoutType from '../../hooks/useLayoutType'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate, push, goBack } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import Style, { gutters, size } from '../../styles/Style'
import { getArtistDisplayName } from '../../utils/artistName'
import { getProjectLikes, LIKE_TARGET } from '../../utils/likes'
import MusicCard from '../Library/components/MusicCard'
const Profile = () => {
const { params } = useRoute();
const [selected, setSelected] = useState("Chansons");
const { isWeb } = useLayoutType();
const { params } = useRoute()
const [selected, setSelected] = useState('Chansons')
const { isWeb } = useLayoutType()
const {
userProjects: selfProjects,
userPlaybacks: selfPlaybacks,
@@ -55,72 +51,65 @@ const Profile = () => {
getUserByUid,
followUser,
unfollowUser,
} = useUser();
const [currentUID] = useGlobal("currentUID");
const targetUserId = params?.userId || currentUID || null;
const isSelf = !!currentUID && targetUserId === currentUID;
const [, setTooltip] = useGlobal("_tooltip");
const [userData, setUserData] = useState(null);
const [isFollowing, setIsFollowing] = useState(false);
const [followers, setFollowers] = useState(0);
const [following, setFollowing] = useState(0);
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [updatingPhoto, setUpdatingPhoto] = useState(false);
const [webUploadMessage, setWebUploadMessage] = useState("");
} = useUser()
const [currentUID] = useGlobal('currentUID')
const targetUserId = params?.userId || currentUID || null
const isSelf = !!currentUID && targetUserId === currentUID
const [, setTooltip] = useGlobal('_tooltip')
const [userData, setUserData] = useState(null)
const [isFollowing, setIsFollowing] = useState(false)
const [followers, setFollowers] = useState(0)
const [following, setFollowing] = useState(0)
const [menuPosition, setMenuPosition] = useState(null)
const [showMenu, setShowMenu] = useState(false)
const [selectedProjectId, setSelectedProjectId] = useState(null)
const [updatingPhoto, setUpdatingPhoto] = useState(false)
const [webUploadMessage, setWebUploadMessage] = useState('')
const selfDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, "MusicLand"),
[currentUserData],
);
const targetDisplayName = useMemo(
() => getArtistDisplayName(userData, "MusicLand"),
[userData],
);
() => getArtistDisplayName(currentUserData, 'MusicLand'),
[currentUserData]
)
const targetDisplayName = useMemo(() => getArtistDisplayName(userData, 'MusicLand'), [userData])
const profileTitle = useMemo(
() => getArtistDisplayName(isSelf ? currentUserData : userData, "Profil"),
[currentUserData, isSelf, userData],
);
const navigateToMusicDetails = useNavigateToMusicDetails();
() => getArtistDisplayName(isSelf ? currentUserData : userData, 'Profil'),
[currentUserData, isSelf, userData]
)
const navigateToMusicDetails = useNavigateToMusicDetails()
const onPressMenu = (item) => {
setSelected(item);
};
setSelected(item)
}
const handleOpenSettings = () => {
navigate(Routes.Settings);
};
navigate(Routes.Settings)
}
// Chargement des données utilisateur si on consulte un autre profil
useEffect(() => {
const fetch = async () => {
if (!targetUserId) return;
if (!targetUserId) return
if (isSelf) {
setUserData(currentUserData || null);
setUserData(currentUserData || null)
setFollowers(
Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy.length
: 0,
);
return;
Array.isArray(currentUserData?.followedBy) ? currentUserData.followedBy.length : 0
)
return
}
const data = await getUserByUid(targetUserId);
setUserData(data || null);
const list = Array.isArray(data?.followedBy) ? data.followedBy : [];
setFollowers(list.length);
setIsFollowing(currentUID ? list.includes(currentUID) : false);
};
fetch();
}, [targetUserId, isSelf, currentUserData?.followedBy?.length || 0]);
const data = await getUserByUid(targetUserId)
setUserData(data || null)
const list = Array.isArray(data?.followedBy) ? data.followedBy : []
setFollowers(list.length)
setIsFollowing(currentUID ? list.includes(currentUID) : false)
}
fetch()
}, [targetUserId, isSelf, currentUserData?.followedBy?.length || 0])
// Live following count pour l'utilisateur consulté (autre que soi)
const followingQueryRef = useMemo(() => {
try {
return targetUserId
? usersRef.where("followedBy", "array-contains", targetUserId)
: null;
return targetUserId ? usersRef.where('followedBy', 'array-contains', targetUserId) : null
} catch (e) {
return null;
return null
}
}, [targetUserId]);
}, [targetUserId])
useDataFromRef({
ref: followingQueryRef,
@@ -129,47 +118,47 @@ const Profile = () => {
condition: !!followingQueryRef && !isSelf,
refreshArray: [targetUserId],
onUpdate: (list) => setFollowing(Array.isArray(list) ? list.length : 0),
});
})
const handleFollowUser = async () => {
if (!currentUID || !targetUserId || isSelf) return;
if (!currentUID || !targetUserId || isSelf) return
if (isFollowing) {
await unfollowUser(targetUserId);
setIsFollowing(false);
setFollowers((v) => Math.max(0, (v || 0) - 1));
await unfollowUser(targetUserId)
setIsFollowing(false)
setFollowers((v) => Math.max(0, (v || 0) - 1))
} else {
await followUser(targetUserId);
setIsFollowing(true);
setFollowers((v) => (v || 0) + 1);
await followUser(targetUserId)
setIsFollowing(true)
setFollowers((v) => (v || 0) + 1)
}
};
}
const onChangeProfilePicture = async (message = "") => {
if (!isSelf || updatingPhoto) return;
const onChangeProfilePicture = async (message = '') => {
if (!isSelf || updatingPhoto) return
try {
setUpdatingPhoto(true);
setWebUploadMessage(isWeb && message ? message : "");
const uid = currentUID;
if (!uid) return;
setUpdatingPhoto(true)
setWebUploadMessage(isWeb && message ? message : '')
const uid = currentUID
if (!uid) return
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 4],
quality: 1,
});
})
const pickedUri = result?.assets?.[0]?.uri || null;
if (!pickedUri) return;
const pickedUri = result?.assets?.[0]?.uri || null
if (!pickedUri) return
const { resultURI = null } = await uploadFileToFirebase({
uri: pickedUri,
path: `users/${uid}/profilePicture.png`,
});
})
if (!resultURI) {
throw new Error("Téléversement de l'image impossible");
throw new Error("Téléversement de l'image impossible")
}
await usersRef.doc(uid).set(
@@ -177,32 +166,28 @@ const Profile = () => {
profilePictureURL: resultURI,
updatedAt: serverTimestamp(),
},
{ merge: true },
);
{ merge: true }
)
const authUser = firebase.auth().currentUser;
const authUser = firebase.auth().currentUser
if (authUser) {
await authUser.updateProfile({ photoURL: resultURI });
await authUser.updateProfile({ photoURL: resultURI })
}
setTooltip({ text: "Photo de profil mise à jour", type: "success" });
setTooltip({ text: 'Photo de profil mise à jour', type: 'success' })
} catch (e) {
setTooltip({
text: e?.message || "Erreur changement photo de profil",
type: "error",
});
text: e?.message || 'Erreur changement photo de profil',
type: 'error',
})
} finally {
setUpdatingPhoto(false);
setWebUploadMessage("");
setUpdatingPhoto(false)
setWebUploadMessage('')
}
};
}
// Composant factorisé pour afficher la liste de musiques
const MusicListSection = ({
data,
emptyText,
likeTarget = LIKE_TARGET.SONG,
}) => (
const MusicListSection = ({ data, emptyText, likeTarget = LIKE_TARGET.SONG }) => (
<ScrollView style={{ flex: 1, marginBottom: 70 }}>
{Array.isArray(data) && data.length > 0 ? (
<FlatList
@@ -214,7 +199,7 @@ const Profile = () => {
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
title={item?.title || 'Sans titre'}
subtitle={isSelf ? selfDisplayName : targetDisplayName}
imageUri={item?.coverUrl || null}
projectId={item?.id}
@@ -227,21 +212,16 @@ const Profile = () => {
project: item,
queueProjects: data,
queueSource: {
id: isSelf ? "profile-self" : "profile-other",
type: "collection",
name: isSelf
? "Mes musiques"
: targetDisplayName || "Profil",
id: isSelf ? 'profile-self' : 'profile-other',
type: 'collection',
name: isSelf ? 'Mes musiques' : targetDisplayName || 'Profil',
},
})
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuPosition(posTop);
setShowMenu(
(prev) =>
!prev || posTop?.top !== (menuPosition?.top ?? null),
);
setSelectedProjectId(item.id)
setMenuPosition(posTop)
setShowMenu((prev) => !prev || posTop?.top !== (menuPosition?.top ?? null))
}}
/>
)}
@@ -260,26 +240,25 @@ const Profile = () => {
isSelf
? [
{
label: "Supprimer",
label: 'Supprimer',
onPress: () =>
SheetManager.show("Delete", {
SheetManager.show('Delete', {
payload: {
title: "Supprimer le projet",
message:
"Cette action supprimera définitivement ce projet.",
title: 'Supprimer le projet',
message: 'Cette action supprimera définitivement ce projet.',
onConfirm: async () => {
try {
if (!selectedProjectId) return;
await projectsRef.doc(selectedProjectId).delete();
if (!selectedProjectId) return
await projectsRef.doc(selectedProjectId).delete()
setTooltip({
type: "success",
text: "Projet supprimé",
});
type: 'success',
text: 'Projet supprimé',
})
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
type: 'error',
text: e?.message || 'Suppression impossible',
})
}
},
},
@@ -290,7 +269,7 @@ const Profile = () => {
}
/>
</ScrollView>
);
)
const EmptyText = ({ text }) => (
<View style={{ flex: 1, ...Style.containerCenter, paddingTop: 20 }}>
@@ -305,19 +284,17 @@ const Profile = () => {
{text}
</Text>
</View>
);
)
// Données projets/profils selon si c'est soi ou un autre utilisateur
const projectsQueryRef = useMemo(() => {
try {
if (!targetUserId || isSelf) return null;
return projectsRef
.where("userId", "==", targetUserId)
.orderBy("updatedAt", "desc");
if (!targetUserId || isSelf) return null
return projectsRef.where('userId', '==', targetUserId).orderBy('updatedAt', 'desc')
} catch (e) {
return null;
return null
}
}, [targetUserId, isSelf]);
}, [targetUserId, isSelf])
const { data: otherUserProjects = [] } = useDataFromRef({
ref: projectsQueryRef,
@@ -325,26 +302,26 @@ const Profile = () => {
listener: true,
condition: !!projectsQueryRef,
refreshArray: [targetUserId],
});
})
const displayedProjects = isSelf ? selfProjects : otherUserProjects;
const displayedPlaybacksSource = isSelf ? selfPlaybacks : otherUserProjects;
const displayedProjects = isSelf ? selfProjects : otherUserProjects
const displayedPlaybacksSource = isSelf ? selfPlaybacks : otherUserProjects
const displayedPlaybacks = Array.isArray(displayedPlaybacksSource)
? displayedPlaybacksSource.filter((project) => project?.playbackUrl)
: [];
const showHeader = isWeb;
const showBackButton = !params?.noBack && !isWeb;
: []
const showHeader = isWeb
const showBackButton = !params?.noBack && !isWeb
return (
<Page
backgroundImg={isWeb ? background.profileWebBG : background.profileWebBG}
headerType={showHeader ? "NAVIGATE" : "NONE"}
headerType={showHeader ? 'NAVIGATE' : 'NONE'}
hideBackButton={params?.noBack}
contentContainerStyle={{
paddingBottom: gutters * 2,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
paddingTop: showHeader ? undefined : 0,
}}
>
@@ -352,16 +329,8 @@ const Profile = () => {
<View style={styles.mobileTopRow}>
<View style={styles.mobileTopLeft}>
{showBackButton ? (
<Pressable
onPress={goBack}
hitSlop={16}
style={styles.backButton}
>
<Image
source={icons.chevronDown}
style={styles.backIcon}
resizeMode="contain"
/>
<Pressable onPress={goBack} hitSlop={16} style={styles.backButton}>
<Image source={icons.chevronDown} style={styles.backIcon} resizeMode="contain" />
</Pressable>
) : null}
<MobileCoinBadge />
@@ -370,7 +339,7 @@ const Profile = () => {
</View>
) : null}
<View style={{ flex: 1, marginTop: responsiveHeight(2), gap: 20 }}>
<View style={{ borderRadius: 20, overflow: "hidden" }}>
<View style={{ borderRadius: 20, overflow: 'hidden' }}>
<BlurView
intensity={20}
style={{
@@ -382,7 +351,7 @@ const Profile = () => {
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ alignItems: "center" }}>
<View style={{ alignItems: 'center' }}>
{isSelf && (
<PressableScale
onPress={handleOpenSettings}
@@ -400,16 +369,12 @@ const Profile = () => {
: userData?.profilePictureURL || null
}
size={108}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
{isSelf && (
<Pressable
style={styles.editAvatarButton}
onPress={() =>
onChangeProfilePicture(
loaderMessages.profilePhotoUploadWeb,
)
}
onPress={() => onChangeProfilePicture(loaderMessages.profilePhotoUploadWeb)}
disabled={updatingPhoto}
hitSlop={10}
>
@@ -448,42 +413,36 @@ const Profile = () => {
paddingHorizontal: isWeb && 30,
}}
>
<View style={{ flex: 1, alignItems: "center" }}>
<View style={{ flex: 1, alignItems: 'center' }}>
<Text style={styles.value}>
{Array.isArray(displayedProjects)
? displayedProjects.length
: 0}
{Array.isArray(displayedProjects) ? displayedProjects.length : 0}
</Text>
<Text style={styles.label}>projets</Text>
</View>
<Pressable
style={{ flex: 1, alignItems: "center" }}
style={{ flex: 1, alignItems: 'center' }}
onPress={() =>
push(Routes.Follows, {
selected: "Abonnés",
selected: 'Abonnés',
...(isSelf ? {} : { userId: targetUserId }),
})
}
>
<Text style={styles.value}>
{isSelf
? currentUserData?.followedBy?.length || 0
: followers}
{isSelf ? currentUserData?.followedBy?.length || 0 : followers}
</Text>
<Text style={styles.label}>abonnés</Text>
</Pressable>
<Pressable
style={{ flex: 1, alignItems: "center" }}
style={{ flex: 1, alignItems: 'center' }}
onPress={() =>
push(Routes.Follows, {
selected: "Abonnements",
selected: 'Abonnements',
...(isSelf ? {} : { userId: targetUserId }),
})
}
>
<Text style={styles.value}>
{isSelf ? followingCount : following}
</Text>
<Text style={styles.value}>{isSelf ? followingCount : following}</Text>
<Text style={styles.label}>abonnements</Text>
</Pressable>
</View>
@@ -493,12 +452,12 @@ const Profile = () => {
style={{
height: 36,
borderRadius: 100,
overflow: "hidden",
backgroundColor: "#FFFFFF22",
alignItems: "center",
justifyContent: "center",
width: "80%",
alignSelf: "center",
overflow: 'hidden',
backgroundColor: '#FFFFFF22',
alignItems: 'center',
justifyContent: 'center',
width: '80%',
alignSelf: 'center',
marginTop: 18,
}}
>
@@ -509,7 +468,7 @@ const Profile = () => {
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{isFollowing ? "Suivi(e)" : "Suivre"}
{isFollowing ? 'Suivi(e)' : 'Suivre'}
</Text>
</View>
</Pressable>
@@ -523,18 +482,12 @@ const Profile = () => {
}}
>
{/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
{["Chansons", "Playback"].map((item, index) => (
<Pressable
key={index}
onPress={() => onPressMenu(item)}
style={{ flex: 1 }}
>
{['Chansons', 'Playback'].map((item, index) => (
<Pressable key={index} onPress={() => onPressMenu(item)} style={{ flex: 1 }}>
<BorderGradient
gradientProps={{
colors:
selected === item
? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran],
selected === item ? ['#FFFFFF00', '#FFFFFF'] : [Palette.tran, Palette.tran],
locations: [0, 1],
start: { x: 0, y: 0 },
end: { x: 1, y: 0 },
@@ -555,42 +508,39 @@ const Profile = () => {
</Pressable>
))}
</View>
{selected === "Playback" && (
{selected === 'Playback' && (
<MusicListSection
data={displayedPlaybacks}
emptyText="Aucun playback pour le moment"
likeTarget={LIKE_TARGET.PLAYBACK}
/>
)}
{selected === "Chansons" && (
<MusicListSection
data={displayedProjects}
emptyText="Aucune chanson pour le moment"
/>
{selected === 'Chansons' && (
<MusicListSection data={displayedProjects} emptyText="Aucune chanson pour le moment" />
)}
{selected === "Clips" && (
{selected === 'Clips' && (
<View style={{ flex: 1 }}>
<EmptyText text={"Aucun clip pour le moment"} />
<EmptyText text={'Aucun clip pour le moment'} />
</View>
)}
</View>
</Page>
);
};
)
}
export default Profile;
export default Profile
const styles = StyleSheet.create({
mobileTopRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
mobileTopLeft: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flexShrink: 1,
},
@@ -604,7 +554,7 @@ const styles = StyleSheet.create({
backIcon: {
...Style.iconSmall,
...Style.mirrorHorizontal,
transform: [{ rotate: "90deg" }],
transform: [{ rotate: '90deg' }],
},
value: {
fontSize: 16,
@@ -625,31 +575,31 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderRadius: 10,
height: 33,
position: "absolute",
position: 'absolute',
zIndex: 1,
width: "100%",
width: '100%',
},
blurContainer: {
height: 33,
zIndex: -1,
borderRadius: 10,
overflow: "hidden",
overflow: 'hidden',
backgroundColor: Palette.glass,
},
blurView: {
width: "100%",
height: "100%",
width: '100%',
height: '100%',
paddingHorizontal: 10,
...Style.containerCenter,
},
avatarWrapper: {
position: "relative",
position: 'relative',
...size({ size: 108 }),
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
settingsButton: {
position: "absolute",
position: 'absolute',
top: -6,
right: -6,
width: 36,
@@ -662,7 +612,7 @@ const styles = StyleSheet.create({
...Style.containerCenter,
},
editAvatarButton: {
position: "absolute",
position: 'absolute',
bottom: 4,
right: 4,
},
@@ -684,7 +634,7 @@ const styles = StyleSheet.create({
color: Palette.white,
fontSize: 12,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
opacity: 0.85,
},
});
})
+21 -28
View File
@@ -1,23 +1,23 @@
import { View, Text, Pressable, Image } from "react-native";
import React, { useState } from "react";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { icons, img } from "../../assets";
import Style, { gutters, size } from "../../styles/Style";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { goBack } from "../../navigation/NavigationService";
import { SheetManager } from "react-native-actions-sheet";
import { View, Text, Pressable, Image } from 'react-native'
import React, { useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { icons, img } from '../../assets'
import Style, { gutters, size } from '../../styles/Style'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { goBack } from '../../navigation/NavigationService'
import { SheetManager } from 'react-native-actions-sheet'
const Reels = () => {
const { top } = useSafeAreaInsets();
const [isFav, setIsFav] = useState(false);
const { top } = useSafeAreaInsets()
const [isFav, setIsFav] = useState(false)
return (
<View style={{ flex: 1 }}>
<View
style={{
...Style.containerRow,
position: "absolute",
position: 'absolute',
top: top + 10,
zIndex: 1,
paddingHorizontal: gutters,
@@ -29,7 +29,7 @@ const Reels = () => {
source={icons.chevronDown}
style={{
...size({ size: 15 }),
transform: [{ rotate: "90deg" }],
transform: [{ rotate: '90deg' }],
}}
resizeMode="contain"
/>
@@ -44,19 +44,16 @@ const Reels = () => {
>
Mon Profile
</Text>
<View style={{ flex: 1, alignItems: "flex-end" }}>
<Pressable onPress={() => SheetManager.show("DeletePlayback")}>
<View style={{ flex: 1, alignItems: 'flex-end' }}>
<Pressable onPress={() => SheetManager.show('DeletePlayback')}>
<Image source={icons.trash} />
</Pressable>
</View>
</View>
<Image
source={img.placeholder3}
style={{ width: "100%", height: "100%" }}
/>
<Image source={img.placeholder3} style={{ width: '100%', height: '100%' }} />
<View
style={{
position: "absolute",
position: 'absolute',
zIndex: 1,
bottom: gutters * 2,
right: 33,
@@ -71,15 +68,11 @@ const Reels = () => {
/>
</Pressable>
<Pressable>
<Image
source={icons.share}
style={size({ size: 26 })}
resizeMode="contain"
/>
<Image source={icons.share} style={size({ size: 26 })} resizeMode="contain" />
</Pressable>
</View>
</View>
);
};
)
}
export default Reels;
export default Reels
+38 -42
View File
@@ -1,68 +1,68 @@
import React from "react";
import { View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import BlurItemButton from "../../components/BlurItemButton";
import BorderGradientButton from "../../components/BorderGradientButton";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import React from 'react'
import { View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import BlurItemButton from '../../components/BlurItemButton'
import BorderGradientButton from '../../components/BorderGradientButton'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUserData } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
const SETTINGS = [
{
title: "Adresse mail",
title: 'Adresse mail',
action: () => {
navigate(Routes.ChangeEmailAddress);
navigate(Routes.ChangeEmailAddress)
},
},
{
title: "Modifier mon mot de passe",
title: 'Modifier mon mot de passe',
action: () => {
navigate(Routes.ChangePassword);
navigate(Routes.ChangePassword)
},
},
{
title: "Notifications",
title: 'Notifications',
action: () => {
navigate(Routes.Notifications);
navigate(Routes.Notifications)
},
},
{
title: "Langue",
title: 'Langue',
action: () => {
navigate(Routes.Language);
navigate(Routes.Language)
},
},
{
title: "Historique de commandes",
title: 'Historique de commandes',
action: () => {
navigate(Routes.OrderHistory);
navigate(Routes.OrderHistory)
},
},
{
title: "Gérer mon abonnement",
title: 'Gérer mon abonnement',
action: () => {
navigate(Routes.ManageSubscription);
navigate(Routes.ManageSubscription)
},
},
];
]
const Settings = () => {
const { onSignOut } = useUserData();
const { onSignOut } = useUserData()
const handleSignOut = async () => {
try {
await onSignOut();
await onSignOut()
} catch (e) {
console.log("Sign out error", e?.message);
console.log('Sign out error', e?.message)
} finally {
navigate(Routes.Login);
navigate(Routes.Login)
}
};
}
return (
<Page
headerType="NAVIGATION"
@@ -72,32 +72,28 @@ const Settings = () => {
paddingBottom: gutters * 2,
}}
containerStyle={{
backgroundColor: isWeb ? "transparent" : "#0000004D",
backgroundColor: isWeb ? 'transparent' : '#0000004D',
}}
>
<View style={{ flex: 1, marginTop: responsiveHeight(2) }}>
<View style={{ flex: 1, gap: 10 }}>
{SETTINGS.map((item, index) => (
<BlurItemButton
key={index}
title={item.title}
onPress={item.action}
/>
<BlurItemButton key={index} title={item.title} onPress={item.action} />
))}
</View>
<View style={{ width: "80%", gap: 5, alignSelf: "center" }}>
<View style={{ width: '80%', gap: 5, alignSelf: 'center' }}>
<BorderGradientButton title="Déconnexion" onPress={handleSignOut} />
<BorderGradientButton
title="Supprimer mon profil"
titleStyle={{
color: Palette.red,
}}
onPress={() => SheetManager.show("DeleteAccount")}
onPress={() => SheetManager.show('DeleteAccount')}
/>
</View>
</View>
</Page>
);
};
)
}
export default Settings;
export default Settings
@@ -1,66 +1,62 @@
import React from "react";
import { StyleSheet, Switch, Text, View } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { Image as ExpoImage } from "expo-image";
import { useNavigation } from "@react-navigation/native";
import React from 'react'
import { StyleSheet, Switch, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons'
import { Image as ExpoImage } from 'expo-image'
import { useNavigation } from '@react-navigation/native'
import ClubCard from "../../Home/components/ClubCard";
import { gutters, Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { Routes } from "../../../navigation/Routes";
import { useUserData } from "../../../providers/UserDataProvider";
import { icons } from "../../../assets";
import { isWeb } from "../../../hooks/useLayoutType";
import ClubCard from '../../Home/components/ClubCard'
import { gutters, Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { Routes } from '../../../navigation/Routes'
import { useUserData } from '../../../providers/UserDataProvider'
import { icons } from '../../../assets'
import { isWeb } from '../../../hooks/useLayoutType'
const ICON_BASE_ACCENT = "#A96BFF";
const ICON_BASE_ACCENT = '#A96BFF'
const CLUB_ADVANTAGES = [
{
title: "Gagne des crédits avec ton abonnement",
description: "Des crédits premium tous les mois pour créer plus.",
iconType: "image",
title: 'Gagne des crédits avec ton abonnement',
description: 'Des crédits premium tous les mois pour créer plus.',
iconType: 'image',
icon: icons.coin,
accent: ICON_BASE_ACCENT,
},
{
title: "Gagne le double de cashprice grace à ton adésion au club",
description: "Cashprice doublé pour les membres du Club Musicland.",
iconType: "vector",
iconName: "chart-line",
accent: "#F8C24D",
title: 'Gagne le double de cashprice grace à ton adésion au club',
description: 'Cashprice doublé pour les membres du Club Musicland.',
iconType: 'vector',
iconName: 'chart-line',
accent: '#F8C24D',
},
];
]
const ClubAdvantagesCard = ({ style, isDev = false }) => {
const navigation = useNavigation();
const { hasActiveSubscription } = useUserData() || {};
const [useWebLayout, setUseWebLayout] = React.useState(isDev ? isWeb : false);
const advantages = CLUB_ADVANTAGES;
const navigation = useNavigation()
const { hasActiveSubscription } = useUserData() || {}
const [useWebLayout, setUseWebLayout] = React.useState(isDev ? isWeb : false)
const advantages = CLUB_ADVANTAGES
React.useEffect(() => {
if (!isDev) {
setUseWebLayout(false);
setUseWebLayout(false)
}
}, [isDev]);
}, [isDev])
const title = hasActiveSubscription
? "Tu profites déjà du Club Musicland"
: "Rejoins le Club Musicland";
? 'Tu profites déjà du Club Musicland'
: 'Rejoins le Club Musicland'
const subtitle = hasActiveSubscription
? "Grâce à ton abonnement, tu bénéficies de tous ces avantages."
: "Découvre ce que tu gagnes en passant au club premium.";
? 'Grâce à ton abonnement, tu bénéficies de tous ces avantages.'
: 'Découvre ce que tu gagnes en passant au club premium.'
const handleOpenPlans = () => {
navigation.navigate(Routes.Payments);
};
navigation.navigate(Routes.Payments)
}
return (
<View
style={[
styles.card,
hasActiveSubscription ? styles.cardActive : styles.cardInactive,
style,
]}
style={[styles.card, hasActiveSubscription ? styles.cardActive : styles.cardInactive, style]}
>
{/* {isDev ? (
<View style={styles.testSwitches}>
@@ -93,17 +89,15 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
]}
>
{advantages.map((advantage) => {
const accent = advantage.accent || Palette.white;
const iconType = advantage.iconType || "image";
const accent = advantage.accent || Palette.white
const iconType = advantage.iconType || 'image'
return (
<View
key={advantage.title}
style={[
styles.advantageCard,
useWebLayout
? styles.advantageCardWeb
: styles.advantageCardMobile,
useWebLayout ? styles.advantageCardWeb : styles.advantageCardMobile,
]}
>
{hasActiveSubscription ? (
@@ -113,9 +107,7 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
color={Palette.green}
style={[
styles.advantageCheck,
useWebLayout
? styles.advantageCheckWeb
: styles.advantageCheckMobile,
useWebLayout ? styles.advantageCheckWeb : styles.advantageCheckMobile,
]}
/>
) : null}
@@ -129,12 +121,8 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
},
]}
>
{iconType === "vector" ? (
<MaterialCommunityIcons
name={advantage.iconName}
size={24}
color={accent}
/>
{iconType === 'vector' ? (
<MaterialCommunityIcons name={advantage.iconName} size={24} color={accent} />
) : (
<ExpoImage
source={advantage.icon}
@@ -147,29 +135,18 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
style={[
styles.advantageContent,
useWebLayout && styles.advantageContentWeb,
hasActiveSubscription &&
!useWebLayout && { paddingRight: 10 },
hasActiveSubscription && !useWebLayout && { paddingRight: 10 },
]}
>
<Text
style={[
styles.advantageTitle,
useWebLayout && styles.advantageTextCenter,
]}
>
<Text style={[styles.advantageTitle, useWebLayout && styles.advantageTextCenter]}>
{advantage.title}
</Text>
<Text
style={[
styles.advantageText,
useWebLayout && styles.advantageTextCenter,
]}
>
<Text style={[styles.advantageText, useWebLayout && styles.advantageTextCenter]}>
{advantage.description}
</Text>
</View>
</View>
);
)
})}
</View>
@@ -179,29 +156,29 @@ const ClubAdvantagesCard = ({ style, isDev = false }) => {
hasActiveSubscription={hasActiveSubscription}
/>
</View>
);
};
)
}
const styles = StyleSheet.create({
card: {
paddingHorizontal: gutters * 1.25,
paddingVertical: gutters,
borderRadius: 24,
backgroundColor: "#252438",
backgroundColor: '#252438',
borderWidth: 1,
gap: gutters,
borderColor: "white",
borderColor: 'white',
},
cardInactive: {
borderColor: "rgba(255, 255, 255, 0.08)",
borderColor: 'rgba(255, 255, 255, 0.08)',
},
cardActive: {
borderColor: Palette.green,
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: gutters,
},
testSwitches: {
@@ -210,90 +187,90 @@ const styles = StyleSheet.create({
padding: gutters * 0.9,
borderRadius: 14,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.12)",
borderColor: 'rgba(255, 255, 255, 0.12)',
},
switchRow: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: gutters * 0.5,
justifyContent: "space-between",
justifyContent: 'space-between',
},
switchLabel: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 11,
color: Palette.grayMid,
textTransform: "uppercase",
textTransform: 'uppercase',
letterSpacing: 0.6,
},
titleBlock: {
flex: 1,
gap: gutters * 0.3,
alignItems: "center",
alignItems: 'center',
},
title: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
},
subtitle: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
color: "rgba(255, 255, 255, 0.82)",
color: 'rgba(255, 255, 255, 0.82)',
lineHeight: 18,
textAlign: "center",
textAlign: 'center',
},
advantagesList: {
gap: gutters * 0.75,
},
advantagesListWeb: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
advantagesListMobile: {
flexDirection: "column",
flexDirection: 'column',
},
advantageCard: {
flexDirection: "row",
alignItems: "flex-start",
flexDirection: 'row',
alignItems: 'flex-start',
gap: gutters * 0.65,
backgroundColor: "rgba(255, 255, 255, 0.08)",
backgroundColor: 'rgba(255, 255, 255, 0.08)',
borderRadius: 16,
borderWidth: 0,
padding: gutters,
shadowColor: "#000",
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.2,
shadowRadius: 10,
elevation: 2,
},
advantageCardWeb: {
flexBasis: "31%",
maxWidth: "32%",
flexBasis: '31%',
maxWidth: '32%',
minWidth: 200,
flexGrow: 1,
flexShrink: 1,
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-start",
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
gap: gutters * 0.5,
position: "relative",
position: 'relative',
},
advantageCardMobile: {
width: "100%",
position: "relative",
width: '100%',
position: 'relative',
},
advantageIcon: {
width: 42,
height: 42,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
},
advantageIconWeb: {
alignSelf: "center",
alignSelf: 'center',
marginBottom: gutters * 0.15,
},
advantageIconImage: {
@@ -305,7 +282,7 @@ const styles = StyleSheet.create({
gap: gutters * 0.2,
},
advantageContentWeb: {
alignItems: "center",
alignItems: 'center',
gap: gutters * 0.4,
},
advantageTitle: {
@@ -317,23 +294,23 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
lineHeight: 18,
color: "rgba(255, 255, 255, 0.85)",
color: 'rgba(255, 255, 255, 0.85)',
},
advantageTextCenter: {
textAlign: "center",
textAlign: 'center',
},
advantageCheck: {
position: "absolute",
position: 'absolute',
},
advantageCheckWeb: {
top: 10,
right: 10,
},
advantageCheckMobile: {
top: "50%",
top: '50%',
right: 12,
transform: [{ translateY: -10 }],
},
});
})
export default ClubAdvantagesCard;
export default ClubAdvantagesCard
+19 -19
View File
@@ -1,19 +1,19 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import { Platform, Pressable, Text, TextInput, View } from "react-native";
import EyeSlashSVG from "../../../assets/UI/EyeSlashSVG";
import EyeSVG from "../../../assets/UI/EyeSVG";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { BlurView } from 'expo-blur'
import React, { useState } from 'react'
import { Platform, Pressable, Text, TextInput, View } from 'react-native'
import EyeSlashSVG from '../../../assets/UI/EyeSlashSVG'
import EyeSVG from '../../../assets/UI/EyeSVG'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
const EditInput = ({
label = "",
placeholder = "",
type = "default",
value = "",
label = '',
placeholder = '',
type = 'default',
value = '',
setValue = () => {},
}) => {
const [showPassword, setShowPassword] = useState(false);
const [showPassword, setShowPassword] = useState(false)
return (
<View style={{ gap: 4 }}>
@@ -42,12 +42,12 @@ const EditInput = ({
</Text>
</View>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={{
paddingHorizontal: 12,
borderRadius: 12,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
height: 52,
...Style.containerRow,
}}
@@ -67,18 +67,18 @@ const EditInput = ({
keyboardType={type}
value={value}
onChangeText={setValue}
{...(type === "password" && {
{...(type === 'password' && {
secureTextEntry: !showPassword,
})}
/>
{type === "password" && (
{type === 'password' && (
<Pressable onPress={() => setShowPassword(!showPassword)}>
{showPassword ? <EyeSVG /> : <EyeSlashSVG />}
</Pressable>
)}
</BlurView>
</View>
);
};
)
}
export default EditInput;
export default EditInput
+40 -43
View File
@@ -1,81 +1,78 @@
import React, { useGlobal } from "reactn";
import React, { useGlobal } from 'reactn'
import Page from "../layouts/Page";
import Page from '../layouts/Page'
import InputRow from "../components/InputRow.js";
import InputRow from '../components/InputRow.js'
import { projectsRef } from "../config/firebase";
import {
checkIfFigmaLinkIsValid,
getValueFromKeyState,
} from "../helpers/index.js";
import { projectsRef } from '../config/firebase'
import { checkIfFigmaLinkIsValid, getValueFromKeyState } from '../helpers/index.js'
export default (props) => {
const [, setIsLoading] = useGlobal("_isLoading");
const [, setTooltip] = useGlobal("_tooltip");
const [, setIsLoading] = useGlobal('_isLoading')
const [, setTooltip] = useGlobal('_tooltip')
const [currentProjectData] = useGlobal("currentProjectData");
const [currentProjectData] = useGlobal('currentProjectData')
const settingsList = [
{
title: "Lien du Figma",
key: "links.figma",
type: "string",
title: 'Lien du Figma',
key: 'links.figma',
type: 'string',
},
{
title: "Lien de test",
key: "links.test",
type: "string",
title: 'Lien de test',
key: 'links.test',
type: 'string',
},
{
title: "Lien de production",
key: "links.production",
type: "string",
title: 'Lien de production',
key: 'links.production',
type: 'string',
},
{
title: "Lien du dashboard",
key: "dashboardURL",
type: "string",
title: 'Lien du dashboard',
key: 'dashboardURL',
type: 'string',
},
{
title: "Lien du git",
key: "links.git",
type: "string",
title: 'Lien du git',
key: 'links.git',
type: 'string',
},
];
]
const onUpdateSetting = async ({ key, value }) => {
try {
setIsLoading(true);
setIsLoading(true)
if (key === "links.figma") {
if (key === 'links.figma') {
if (!checkIfFigmaLinkIsValid({ figmaLink: value })) {
throw new Error("Le lien Figma n'est pas valide");
throw new Error("Le lien Figma n'est pas valide")
}
}
await projectsRef.doc(currentProjectData.id).update({
[key]: value,
});
})
setTooltip({
type: "success",
text: "Paramètre mis à jour",
});
type: 'success',
text: 'Paramètre mis à jour',
})
} catch (error) {
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
return (
<Page headerType="NAVIGATE" title={"Liens du projet"} scrollEnabled>
<Page headerType="NAVIGATE" title={'Liens du projet'} scrollEnabled>
{settingsList.map((setting) => {
const { title, key, type } = setting;
const { title, key, type } = setting
return (
<InputRow
@@ -86,8 +83,8 @@ export default (props) => {
title={title}
onUpdateValue={onUpdateSetting}
/>
);
)
})}
</Page>
);
};
)
}
+81 -81
View File
@@ -1,158 +1,158 @@
import * as ImagePicker from "expo-image-picker";
import { Pressable, Text } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import React, { useState } from "reactn";
import * as ImagePicker from 'expo-image-picker'
import { Pressable, Text } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import React, { useState } from 'reactn'
import { responsiveWidth } from "../actions/responsiveSizes.js";
import Page from "../layouts/Page";
import { responsiveWidth } from '../actions/responsiveSizes.js'
import Page from '../layouts/Page'
import alert from "../components/Alert.js";
import Avatar from "../components/Avatar";
import InputRow from "../components/InputRow.js";
import ItemRowList from "../components/ItemRowList.js";
import alert from '../components/Alert.js'
import Avatar from '../components/Avatar'
import InputRow from '../components/InputRow.js'
import ItemRowList from '../components/ItemRowList.js'
import { Fonts, gutters, Palette, Style } from "../styles";
import { Fonts, gutters, Palette, Style } from '../styles'
import { projectsRef } from "../config/firebase";
import { projectsRef } from '../config/firebase'
import { getValueFromKeyState } from "../helpers/index.js";
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
import { getValueFromKeyState } from '../helpers/index.js'
import { uploadFileToFirebase } from '../helpers/uploadToFirebase'
import { Routes } from "../navigation/Routes.js";
import { useUserData } from "../providers/UserDataProvider.js";
import { Routes } from '../navigation/Routes.js'
import { useUserData } from '../providers/UserDataProvider.js'
// eslint-disable-next-line react/display-name
export default ({ navigation }) => {
const { setIsLoading, setTooltip } = useMinuit();
const { setIsLoading, setTooltip } = useMinuit()
const [stateToEdit] = useState({});
const [stateToEdit] = useState({})
const { isSuperAdmin } = useUserData();
const { isSuperAdmin } = useUserData()
const settingsList = [
{
title: "Activer le 'shake'",
key: "enableShake",
type: "boolean",
key: 'enableShake',
type: 'boolean',
},
{
title: "Activer les copies de sauvegarde",
key: "enableBackup",
type: "boolean",
title: 'Activer les copies de sauvegarde',
key: 'enableBackup',
type: 'boolean',
},
{
title: "Passer en mode 'maintenance'",
key: "enableMaintenanceMode",
type: "boolean",
key: 'enableMaintenanceMode',
type: 'boolean',
},
{
title: "Identifiant Firebase",
key: "firebaseConfig.projectId",
type: "string",
title: 'Identifiant Firebase',
key: 'firebaseConfig.projectId',
type: 'string',
},
{
title: "Identifiant App Store",
key: "storeConfig.apple.appID",
type: "string",
title: 'Identifiant App Store',
key: 'storeConfig.apple.appID',
type: 'string',
},
{
title: "Identifiant Algolia",
key: "algoliaConfig.appId",
type: "string",
title: 'Identifiant Algolia',
key: 'algoliaConfig.appId',
type: 'string',
},
];
]
const destructiveSettingsList = [
{
title: "Archiver le projet",
title: 'Archiver le projet',
action: () => onArchiveProject(),
textStyle: {
color: Palette.red,
},
condition: isSuperAdmin,
},
].filter(({ condition = true }) => condition);
].filter(({ condition = true }) => condition)
const onChangePicture = async () => {
try {
setIsLoading(true);
setIsLoading(true)
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 4],
quality: 1,
});
})
if (result?.assets?.[0]?.uri) {
const { resultURI = null } = await uploadFileToFirebase({
uri: result?.assets?.[0]?.uri,
path: `projects/${Math.random()}/icon.png`,
});
})
if (resultURI) {
await projectsRef.doc(Math.random()).update({
iconURL: resultURI,
});
})
setTooltip({
type: "success",
text: "Image changée avec succès",
});
type: 'success',
text: 'Image changée avec succès',
})
} else {
throw new Error("Erreur changement d'image");
throw new Error("Erreur changement d'image")
}
}
} catch (error) {
console.log(error);
console.log(error)
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}
const onUpdateSetting = async ({ key, value }) => {
try {
await projectsRef.doc(Math.random()).update({
[key]: value,
});
})
setTooltip({
type: "success",
text: "Paramètre mis à jour",
});
type: 'success',
text: 'Paramètre mis à jour',
})
} catch (error) {
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
}
};
}
const onArchiveProject = async () => {
alert(
"Êtes-vous sûr?",
'Êtes-vous sûr?',
"Pour annuler l'archivage du projet, il faudra contacter le support.",
[
{
text: "Annuler",
text: 'Annuler',
onPress: () => {},
style: "cancel",
style: 'cancel',
},
{
text: "Confirmer",
text: 'Confirmer',
onPress: async () => {
try {
setIsLoading(true);
setIsLoading(true)
setTooltip({
type: "success",
text: "Projet archivé avec succès!",
});
type: 'success',
text: 'Projet archivé avec succès!',
})
navigation.reset({
index: 0,
@@ -161,31 +161,31 @@ export default ({ navigation }) => {
name: Routes.BottomTab,
},
],
});
})
} catch (error) {
setTooltip({
type: "error",
type: 'error',
text: error.message,
});
})
} finally {
setIsLoading(false);
setIsLoading(false)
}
},
style: "confirm",
style: 'confirm',
},
],
{ cancelable: false }
);
};
)
}
return (
<Page headerType="NAVIGATE" title={"Paramètres du projet"} scrollEnabled>
<Page headerType="NAVIGATE" title={'Paramètres du projet'} scrollEnabled>
<Pressable
onPress={onChangePicture}
style={[Style.containerCenter, { marginBottom: gutters }]}
>
<Avatar
name={"random"}
name={'random'}
url={null}
forceRawImage
size={responsiveWidth(30)}
@@ -196,8 +196,8 @@ export default ({ navigation }) => {
/>
<Text
style={Fonts({
type: "section",
style: { color: Palette.primary, textDecorationLine: "underline" },
type: 'section',
style: { color: Palette.primary, textDecorationLine: 'underline' },
})}
>
Modifier
@@ -205,7 +205,7 @@ export default ({ navigation }) => {
</Pressable>
{settingsList.map((setting) => {
const { title, key, type } = setting;
const { title, key, type } = setting
return (
<InputRow
@@ -216,12 +216,12 @@ export default ({ navigation }) => {
title={title}
onUpdateValue={onUpdateSetting}
/>
);
)
})}
{destructiveSettingsList.map((setting, index) => (
<ItemRowList key={index} {...setting} containerStyle={{}} />
))}
</Page>
);
};
)
}
+108 -131
View File
@@ -1,179 +1,160 @@
import { useRoute } from "@react-navigation/native";
import React, { useCallback, useMemo, useState } from "react";
import { Linking, StyleSheet, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background } from "../../assets";
import AppCheckbox from "../../components/AppCheckbox";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import alert from "../../components/Alert";
import firebase, { projectsRef, serverTimestamp } from "../../config/firebase";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { useRoute } from '@react-navigation/native'
import React, { useCallback, useMemo, useState } from 'react'
import { Linking, StyleSheet, Text, View } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { background } from '../../assets'
import AppCheckbox from '../../components/AppCheckbox'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import alert from '../../components/Alert'
import firebase, { projectsRef, serverTimestamp } from '../../config/firebase'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette, gutters } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
const PublishYoutube = () => {
const route = useRoute();
const routeProjectId = route?.params?.projectId ?? null;
const { userProjects = [], selectedProject } = useUser();
const { setTooltip } = useMinuit();
const route = useRoute()
const routeProjectId = route?.params?.projectId ?? null
const { userProjects = [], selectedProject } = useUser()
const { setTooltip } = useMinuit()
const project = useMemo(() => {
if (routeProjectId) {
return (
userProjects.find((item) => item?.id === routeProjectId) ||
selectedProject ||
null
);
return userProjects.find((item) => item?.id === routeProjectId) || selectedProject || null
}
if (selectedProject?.id) {
return selectedProject;
return selectedProject
}
return userProjects.length ? userProjects[0] : null;
}, [routeProjectId, selectedProject, userProjects]);
return userProjects.length ? userProjects[0] : null
}, [routeProjectId, selectedProject, userProjects])
const projectId = project?.id || routeProjectId || null;
const projectId = project?.id || routeProjectId || null
const [saving, setSaving] = useState(false);
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
const youtubeUrl = project?.youtubeUrl ?? null;
const youtubeStatus = project?.youtubeStatus ?? null;
const youtubeError = project?.youtubeError ?? null;
const playbackReady = !!project?.playbackUrl;
const hasYoutubePublication = !!youtubeUrl;
const [saving, setSaving] = useState(false)
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
const youtubeUrl = project?.youtubeUrl ?? null
const youtubeStatus = project?.youtubeStatus ?? null
const youtubeError = project?.youtubeError ?? null
const playbackReady = !!project?.playbackUrl
const hasYoutubePublication = !!youtubeUrl
const isPublishing = useMemo(
() =>
["PUBLISHING", "UPLOADING", "PROCESSING", "QUEUED"].includes(
youtubeStatus
),
() => ['PUBLISHING', 'UPLOADING', 'PROCESSING', 'QUEUED'].includes(youtubeStatus),
[youtubeStatus]
);
)
const publishDisabled =
saving ||
isPublishing ||
!playbackReady ||
!projectId ||
!hasAcceptedPublication;
saving || isPublishing || !playbackReady || !projectId || !hasAcceptedPublication
const publishButtonLabel = isPublishing
? "Publication en cours..."
? 'Publication en cours...'
: hasYoutubePublication
? "Mettre à jour sur YouTube"
: "Publier sur YouTube";
? 'Mettre à jour sur YouTube'
: 'Publier sur YouTube'
const statusMessage = useMemo(() => {
if (!playbackReady) {
return "Termine la création de ton playback avec Theo pour débloquer la publication YouTube.";
return 'Termine la création de ton playback avec Theo pour débloquer la publication YouTube.'
}
if (isPublishing) {
return "La publication de ta vidéo est en cours... Patiente un instant.";
return 'La publication de ta vidéo est en cours... Patiente un instant.'
}
if (youtubeError || youtubeStatus === "FAILED") {
return "La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous.";
if (youtubeError || youtubeStatus === 'FAILED') {
return 'La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous.'
}
if (hasYoutubePublication) {
return "Ta vidéo est publiée sur la chaîne YouTube MusicLand. Tu peux la partager dès maintenant.";
return 'Ta vidéo est publiée sur la chaîne YouTube MusicLand. Tu peux la partager dès maintenant.'
}
return "Partage ton playback sur la chaîne YouTube MusicLand.";
}, [
hasYoutubePublication,
isPublishing,
playbackReady,
youtubeError,
youtubeStatus,
]);
return 'Partage ton playback sur la chaîne YouTube MusicLand.'
}, [hasYoutubePublication, isPublishing, playbackReady, youtubeError, youtubeStatus])
const handleOpenYoutube = useCallback(async () => {
if (!youtubeUrl) {
return;
return
}
try {
await Linking.openURL(youtubeUrl);
await Linking.openURL(youtubeUrl)
} catch (_error) {
setTooltip?.({
type: "error",
type: 'error',
text: "Impossible d'ouvrir le lien YouTube",
});
})
}
}, [setTooltip, youtubeUrl]);
}, [setTooltip, youtubeUrl])
const handleMarkPublished = useCallback(async () => {
if (!projectId) {
setTooltip?.({
type: "error",
text: "Sélectionnez un projet avant de publier",
});
return;
type: 'error',
text: 'Sélectionnez un projet avant de publier',
})
return
}
if (!playbackReady) {
setTooltip?.({
type: "error",
type: 'error',
text: "Aucun playback n'est disponible pour la publication",
});
return;
})
return
}
setSaving(true);
setSaving(true)
try {
const publishCallable = firebase
.functions()
.httpsCallable("youtube-publishPlaybackToYoutube");
await publishCallable({ projectId });
const publishCallable = firebase.functions().httpsCallable('youtube-publishPlaybackToYoutube')
await publishCallable({ projectId })
setTooltip?.({
type: "success",
text: "Publication lancée sur YouTube",
});
type: 'success',
text: 'Publication lancée sur YouTube',
})
} catch (error) {
setTooltip?.({
type: "error",
text: error?.message || "Impossible de lancer la publication",
});
type: 'error',
text: error?.message || 'Impossible de lancer la publication',
})
} finally {
setSaving(false);
setSaving(false)
}
}, [playbackReady, projectId, setTooltip]);
}, [playbackReady, projectId, setTooltip])
const handleConfirmPublish = useCallback(() => {
if (!hasAcceptedPublication) {
setTooltip?.({
type: "error",
text: "Confirme la diffusion sur Musicland et YouTube avant de publier",
});
return;
type: 'error',
text: 'Confirme la diffusion sur Musicland et YouTube avant de publier',
})
return
}
if (publishDisabled) {
return;
return
}
const confirmTitle = hasYoutubePublication
? "Confirmer la mise à jour"
: "Confirmer la publication";
? 'Confirmer la mise à jour'
: 'Confirmer la publication'
const confirmDescription = hasYoutubePublication
? "La vidéo existe déjà sur YouTube. Confirme que tu souhaites remplacer la version publiée."
: "Confirme que tu souhaites publier ce playback sur la chaîne YouTube MusicLand. Il sera visible publiquement.";
? 'La vidéo existe déjà sur YouTube. Confirme que tu souhaites remplacer la version publiée.'
: 'Confirme que tu souhaites publier ce playback sur la chaîne YouTube MusicLand. Il sera visible publiquement.'
alert(confirmTitle, confirmDescription, [
{ text: "Annuler", style: "cancel" },
{ text: 'Annuler', style: 'cancel' },
{
text: hasYoutubePublication ? "Mettre à jour" : "Je confirme",
text: hasYoutubePublication ? 'Mettre à jour' : 'Je confirme',
onPress: handleMarkPublished,
},
]);
])
}, [
handleMarkPublished,
hasAcceptedPublication,
hasYoutubePublication,
publishDisabled,
setTooltip,
]);
])
const handleResetPublication = useCallback(async () => {
if (!projectId) {
setTooltip?.({
type: "error",
text: "Sélectionnez un projet avant de réinitialiser",
});
return;
type: 'error',
text: 'Sélectionnez un projet avant de réinitialiser',
})
return
}
setSaving(true);
setSaving(true)
try {
await projectsRef.doc(projectId).set(
{
@@ -181,25 +162,25 @@ const PublishYoutube = () => {
youtubeUrl: null,
youtubeVideoId: null,
youtubePublishedAt: null,
youtubeStatus: "IDLE",
youtubeStatus: 'IDLE',
youtubeError: null,
updatedAt: serverTimestamp(),
},
{ merge: true }
);
)
setTooltip?.({
type: "success",
text: "Publication YouTube réinitialisée",
});
type: 'success',
text: 'Publication YouTube réinitialisée',
})
} catch (error) {
setTooltip?.({
type: "error",
text: error?.message || "Réinitialisation impossible",
});
type: 'error',
text: error?.message || 'Réinitialisation impossible',
})
} finally {
setSaving(false);
setSaving(false)
}
}, [projectId, setTooltip]);
}, [projectId, setTooltip])
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
@@ -209,21 +190,19 @@ const PublishYoutube = () => {
flex: 1,
paddingHorizontal: gutters,
paddingBottom: gutters * 2,
justifyContent: "space-between",
justifyContent: 'space-between',
}}
>
<View style={{ alignItems: "center", gap: 18 }}>
<View style={{ alignItems: 'center', gap: 18 }}>
<Text
style={{
fontSize: 22,
fontFamily: FONT_FAMILY.InterSemiBold,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
}}
>
{hasYoutubePublication
? "Ta vidéo est déjà sur YouTube"
: "Publier sur YouTube"}
{hasYoutubePublication ? 'Ta vidéo est déjà sur YouTube' : 'Publier sur YouTube'}
</Text>
<Text
style={{
@@ -231,7 +210,7 @@ const PublishYoutube = () => {
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
opacity: 0.9,
textAlign: "center",
textAlign: 'center',
lineHeight: 20,
}}
>
@@ -243,7 +222,7 @@ const PublishYoutube = () => {
fontSize: 13,
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.red,
textAlign: "center",
textAlign: 'center',
}}
>
{youtubeError}
@@ -255,7 +234,7 @@ const PublishYoutube = () => {
fontSize: 13,
fontFamily: FONT_FAMILY.InterMedium,
color: Palette.white,
textAlign: "center",
textAlign: 'center',
opacity: 0.8,
}}
numberOfLines={2}
@@ -268,9 +247,7 @@ const PublishYoutube = () => {
<View style={styles.consentContainer}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() =>
setHasAcceptedPublication((prevState) => !prevState)
}
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
/>
<Text style={styles.consentDescription}>
@@ -300,8 +277,8 @@ const PublishYoutube = () => {
</View>
</View>
</Page>
);
};
)
}
const styles = StyleSheet.create({
consentContainer: {
@@ -313,6 +290,6 @@ const styles = StyleSheet.create({
color: Palette.white,
opacity: 0.8,
},
});
})
export default PublishYoutube;
export default PublishYoutube
+106 -123
View File
@@ -1,8 +1,8 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as AppleAuthentication from "expo-apple-authentication";
import DateTimePicker from "@react-native-community/datetimepicker";
import React, { useCallback, useEffect, useState } from "react";
import { useGlobal } from "reactn";
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as AppleAuthentication from 'expo-apple-authentication'
import DateTimePicker from '@react-native-community/datetimepicker'
import React, { useCallback, useEffect, useState } from 'react'
import { useGlobal } from 'reactn'
import {
ActivityIndicator,
KeyboardAvoidingView,
@@ -11,39 +11,39 @@ import {
StyleSheet,
Text,
View,
} from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import Svg, { Path } from "react-native-svg";
import { background } from "../assets";
import BorderGradientButton from "../components/BorderGradientButton";
import { Input } from "../components/Input";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import WebDateModal from "../components/modal/WebDateModal";
import firebase from "../config/firebase";
import { isWeb } from "../hooks/useLayoutType";
import useSocialAuth from "../hooks/useSocialAuth";
import Page from "../layouts/Page";
import { Routes } from "../navigation";
import { navigate, reset } from "../navigation/NavigationService";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
} from 'react-native'
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'
import Svg, { Path } from 'react-native-svg'
import { background } from '../assets'
import BorderGradientButton from '../components/BorderGradientButton'
import { Input } from '../components/Input'
import ItemContainer from '../components/ItemContainer/ItemContainer'
import WebDateModal from '../components/modal/WebDateModal'
import firebase from '../config/firebase'
import { isWeb } from '../hooks/useLayoutType'
import useSocialAuth from '../hooks/useSocialAuth'
import Page from '../layouts/Page'
import { Routes } from '../navigation'
import { navigate, reset } from '../navigation/NavigationService'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
const LANGUAGE_STORAGE_KEY = 'preferredLanguage'
const Register = () => {
const [email, setEmail] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [preferredLanguage, setPreferredLanguage] = useState(null);
const [birthDate, setBirthDate] = useState(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const [email, setEmail] = useState('')
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [preferredLanguage, setPreferredLanguage] = useState(null)
const [birthDate, setBirthDate] = useState(null)
const [showDatePicker, setShowDatePicker] = useState(false)
const [, setTooltip] = useGlobal('_tooltip')
const afterSocialAuth = useCallback(async () => {
const uid = firebase.auth().currentUser?.uid;
if (!uid) throw new Error("Aucun utilisateur après connexion");
reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
}, []);
const uid = firebase.auth().currentUser?.uid
if (!uid) throw new Error('Aucun utilisateur après connexion')
reset({ index: 0, routes: [{ name: Routes.BottomTab }] })
}, [])
const {
signInWithGoogle,
@@ -52,46 +52,46 @@ const Register = () => {
isLoading: socialLoading,
} = useSocialAuth({
onSuccess: afterSocialAuth,
});
})
const isBusy = socialLoading;
const isBusy = socialLoading
useEffect(() => {
AsyncStorage.getItem(LANGUAGE_STORAGE_KEY)
.then((storedLanguage) => {
if (storedLanguage) {
setPreferredLanguage(storedLanguage);
setPreferredLanguage(storedLanguage)
}
})
.catch((error) => {
console.warn("Register: failed to load preferred language", error);
});
}, []);
console.warn('Register: failed to load preferred language', error)
})
}, [])
const isFormValid =
email.trim().length > 0 &&
firstName.trim().length > 0 &&
lastName.trim().length > 0 &&
birthDate;
birthDate
function isAdult(date) {
if (!date) return false;
const today = new Date();
let age = today.getFullYear() - date.getFullYear();
const m = today.getMonth() - date.getMonth();
if (!date) return false
const today = new Date()
let age = today.getFullYear() - date.getFullYear()
const m = today.getMonth() - date.getMonth()
if (m < 0 || (m === 0 && today.getDate() < date.getDate())) {
age--;
age--
}
return age >= 18;
return age >= 18
}
const handleNext = () => {
if (!isAdult(birthDate)) {
setTooltip({
text: "Vous devez avoir au moins 18 ans pour créer un compte",
type: "error",
});
return;
text: 'Vous devez avoir au moins 18 ans pour créer un compte',
type: 'error',
})
return
}
navigate(Routes.CreatePassword, {
email: email.trim(),
@@ -99,24 +99,24 @@ const Register = () => {
lastName: lastName.trim(),
birthDate: birthDate ? birthDate.toISOString() : null,
preferredLanguage,
});
};
})
}
const onDateChange = (event, selectedDate) => {
setShowDatePicker(Platform.OS === "ios");
setShowDatePicker(Platform.OS === 'ios')
if (selectedDate) {
setBirthDate(selectedDate);
setBirthDate(selectedDate)
}
};
}
const renderFormContent = () => (
<View style={styles.formContent}>
<Text style={styles.title}>Renseigne tes informations</Text>
{preferredLanguage && !isWeb ? (
<Text style={styles.languageInfo}>
Langue choisie :{" "}
Langue choisie :{' '}
<Text style={styles.languageInfoValue}>
{preferredLanguage === "fr" ? "Français" : "English"}
{preferredLanguage === 'fr' ? 'Français' : 'English'}
</Text>
</Text>
) : null}
@@ -128,13 +128,7 @@ const Register = () => {
value={firstName}
setValue={setFirstName}
/>
<Input
placeholder="Nom"
label="Nom"
isBlur
value={lastName}
setValue={setLastName}
/>
<Input placeholder="Nom" label="Nom" isBlur value={lastName} setValue={setLastName} />
<Input
placeholder="Adresse mail"
label="Adresse mail"
@@ -151,7 +145,7 @@ const Register = () => {
placeholder="Date de naissance"
label="Date de naissance"
isBlur
value={birthDate ? birthDate.toLocaleDateString() : ""}
value={birthDate ? birthDate.toLocaleDateString() : ''}
editable={false}
/>
</View>
@@ -171,7 +165,7 @@ const Register = () => {
placeholder="Date de naissance"
label="Date de naissance"
isBlur
value={birthDate ? birthDate.toLocaleDateString() : ""}
value={birthDate ? birthDate.toLocaleDateString() : ''}
editable={false}
/>
</View>
@@ -193,43 +187,32 @@ const Register = () => {
<BorderGradientButton
title="Créer mon compte"
containerStyle={{
width: "90%",
alignSelf: "center",
width: '90%',
alignSelf: 'center',
}}
onPress={handleNext}
disabled={!isFormValid || isBusy}
/>
<View style={styles.socialWrapper}>
{isAppleSignInAvailable ? (
<AppleAuthButton
onPress={signInWithApple}
disabled={isBusy}
loading={socialLoading}
/>
<AppleAuthButton onPress={signInWithApple} disabled={isBusy} loading={socialLoading} />
) : null}
<GoogleAuthButton
onPress={signInWithGoogle}
disabled={isBusy}
loading={socialLoading}
/>
<GoogleAuthButton onPress={signInWithGoogle} disabled={isBusy} loading={socialLoading} />
</View>
<View style={styles.footer}>
<Text style={styles.terms}>
En t’inscrivant, tu acceptes nos conditions générales{"\n"}
En t’inscrivant, tu acceptes nos conditions générales{'\n'}
d’utilisation et notre politique de confidentialité.
</Text>
</View>
<Text style={styles.bottomFooterText}>
Tu as déjà un compte ?{" "}
<Text
style={styles.bottomFooterLink}
onPress={() => navigate(Routes.Login)}
>
Tu as déjà un compte ?{' '}
<Text style={styles.bottomFooterLink} onPress={() => navigate(Routes.Login)}>
Se connecter
</Text>
</Text>
</View>
);
)
return (
<Page
@@ -248,7 +231,7 @@ const Register = () => {
renderFormContent()
) : (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.mobileAvoider}
>
<KeyboardAwareScrollView
@@ -263,12 +246,12 @@ const Register = () => {
</ItemContainer>
</View>
</Page>
);
};
)
}
const AppleAuthButton = ({ onPress, disabled, loading }) => {
if (isWeb) {
return null;
return null
}
return (
@@ -287,8 +270,8 @@ const AppleAuthButton = ({ onPress, disabled, loading }) => {
</View>
) : null}
</View>
);
};
)
}
const GoogleAuthButton = ({ onPress, disabled, loading }) => (
<Pressable
@@ -313,7 +296,7 @@ const GoogleAuthButton = ({ onPress, disabled, loading }) => (
)}
</View>
</Pressable>
);
)
const GoogleLogo = () => (
<Svg width={24} height={24} viewBox="0 0 18 18">
@@ -334,22 +317,22 @@ const GoogleLogo = () => (
d="M9 3.54c1.32 0 2.5.45 3.43 1.33l2.57-2.57C13.47.9 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.02 2.32A5.31 5.31 0 0 1 9 3.54Z"
/>
</Svg>
);
)
export default Register;
export default Register
const styles = StyleSheet.create({
pageContent: {
flex: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
},
webItemContainer: {
width: "100%",
width: '100%',
maxWidth: 680,
},
mobileItemContainer: {
width: "100%",
width: '100%',
minHeight: 720,
},
mobileAvoider: {
@@ -357,7 +340,7 @@ const styles = StyleSheet.create({
},
mobileScrollContent: {
flexGrow: 1,
justifyContent: "center",
justifyContent: 'center',
paddingVertical: 24,
},
formContent: {
@@ -380,14 +363,14 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterSemiBold,
},
footer: {
alignItems: "center",
alignItems: 'center',
gap: 4,
},
terms: {
fontSize: 12,
color: Palette.gray,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
},
loginText: {
fontSize: 14,
@@ -406,7 +389,7 @@ const styles = StyleSheet.create({
fontSize: 15,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
textAlign: 'center',
},
bottomFooterLink: {
fontFamily: FONT_FAMILY.InterBold,
@@ -414,49 +397,49 @@ const styles = StyleSheet.create({
fontSize: 16,
},
socialWrapper: {
width: "100%",
width: '100%',
gap: 16,
alignItems: "center",
alignItems: 'center',
},
socialButton: {
alignSelf: "center",
alignSelf: 'center',
borderRadius: 12,
width: "90%",
width: '90%',
backgroundColor: Palette.white,
borderWidth: 1,
borderColor: "#E3E7EF",
borderColor: '#E3E7EF',
paddingVertical: 12,
paddingHorizontal: 20,
shadowColor: "rgba(12, 6, 34, 0.08)",
shadowColor: 'rgba(12, 6, 34, 0.08)',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 2,
},
socialButtonPressed: {
backgroundColor: "#F4F7FF",
backgroundColor: '#F4F7FF',
},
socialButtonDisabled: {
opacity: 0.7,
},
socialButtonContent: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
},
socialIconContainer: {
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
width: 28,
height: 28,
},
socialButtonText: {
flex: 1,
textAlign: "center",
textAlign: 'center',
fontSize: 15,
color: "#202124",
color: '#202124',
fontFamily: FONT_FAMILY.InterSemiBold,
},
socialRightSpacer: {
@@ -464,18 +447,18 @@ const styles = StyleSheet.create({
height: 24,
},
nativeButtonWrapper: {
alignSelf: "center",
width: "90%",
alignSelf: 'center',
width: '90%',
height: 50,
justifyContent: "center",
justifyContent: 'center',
},
nativeButton: {
width: "100%",
width: '100%',
height: 50,
},
nativeLoadingOverlay: {
...StyleSheet.absoluteFillObject,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
});
})
+99 -110
View File
@@ -1,150 +1,142 @@
import { useRoute } from "@react-navigation/native";
import React, { useEffect, useMemo, useState, useGlobal } from "reactn";
import { Text, View } from "react-native";
import {
checkIfPasswordIsStrongEnough,
handleFirebaseError,
} from "../actions/signupActions";
import { background } from "../assets";
import GradientButton from "../components/GradientButton";
import { Input } from "../components/Input";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import firebase from "../config/firebase";
import Page from "../layouts/Page";
import { isWeb } from "../hooks/useLayoutType";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { useRoute } from '@react-navigation/native'
import React, { useEffect, useMemo, useState, useGlobal } from 'reactn'
import { Text, View } from 'react-native'
import { checkIfPasswordIsStrongEnough, handleFirebaseError } from '../actions/signupActions'
import { background } from '../assets'
import GradientButton from '../components/GradientButton'
import { Input } from '../components/Input'
import ItemContainer from '../components/ItemContainer/ItemContainer'
import firebase from '../config/firebase'
import Page from '../layouts/Page'
import { isWeb } from '../hooks/useLayoutType'
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService'
import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
const PASSWORD_ERROR_MESSAGE =
"Ton mot de passe doit contenir au moins 6 caractères et combiner plusieurs types de caractères.";
const PASSWORD_MISMATCH_MESSAGE = "Les mots de passe ne correspondent pas.";
'Ton mot de passe doit contenir au moins 6 caractères et combiner plusieurs types de caractères.'
const PASSWORD_MISMATCH_MESSAGE = 'Les mots de passe ne correspondent pas.'
const INVALID_LINK_MESSAGE =
"Ce lien de réinitialisation n'est plus valide. Demande un nouveau mot de passe.";
"Ce lien de réinitialisation n'est plus valide. Demande un nouveau mot de passe."
const ResetPassword = () => {
const route = useRoute();
const oobCode = (route?.params?.oobCode || "").trim();
const route = useRoute()
const oobCode = (route?.params?.oobCode || '').trim()
const [, setTooltip] = useGlobal("_tooltip");
const [, setIsLoading] = useGlobal("_isLoading");
const [, setTooltip] = useGlobal('_tooltip')
const [, setIsLoading] = useGlobal('_isLoading')
const [email, setEmail] = useState(route?.params?.email || "");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordError, setPasswordError] = useState("");
const [confirmError, setConfirmError] = useState("");
const [codeError, setCodeError] = useState("");
const [isVerifyingCode, setIsVerifyingCode] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [email, setEmail] = useState(route?.params?.email || '')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [passwordError, setPasswordError] = useState('')
const [confirmError, setConfirmError] = useState('')
const [codeError, setCodeError] = useState('')
const [isVerifyingCode, setIsVerifyingCode] = useState(true)
const [isSubmitting, setIsSubmitting] = useState(false)
const trimmedPassword = useMemo(() => (password || "").trim(), [password]);
const trimmedConfirmPassword = useMemo(
() => (confirmPassword || "").trim(),
[confirmPassword]
);
const trimmedPassword = useMemo(() => (password || '').trim(), [password])
const trimmedConfirmPassword = useMemo(() => (confirmPassword || '').trim(), [confirmPassword])
const isPasswordValid = useMemo(() => {
if (!trimmedPassword.length) return false;
return checkIfPasswordIsStrongEnough({ password: trimmedPassword });
}, [trimmedPassword]);
if (!trimmedPassword.length) return false
return checkIfPasswordIsStrongEnough({ password: trimmedPassword })
}, [trimmedPassword])
const doPasswordsMatch = useMemo(() => {
if (!trimmedPassword.length || !trimmedConfirmPassword.length) return false;
return trimmedPassword === trimmedConfirmPassword;
}, [trimmedPassword, trimmedConfirmPassword]);
if (!trimmedPassword.length || !trimmedConfirmPassword.length) return false
return trimmedPassword === trimmedConfirmPassword
}, [trimmedPassword, trimmedConfirmPassword])
useEffect(() => {
const verifyCode = async () => {
if (!oobCode) {
setCodeError(INVALID_LINK_MESSAGE);
setTooltip({ text: INVALID_LINK_MESSAGE, type: "error" });
setIsVerifyingCode(false);
return;
setCodeError(INVALID_LINK_MESSAGE)
setTooltip({ text: INVALID_LINK_MESSAGE, type: 'error' })
setIsVerifyingCode(false)
return
}
try {
setIsLoading(true);
const resetEmail = await firebase
.auth()
.verifyPasswordResetCode(oobCode);
setEmail(resetEmail);
setCodeError("");
setIsLoading(true)
const resetEmail = await firebase.auth().verifyPasswordResetCode(oobCode)
setEmail(resetEmail)
setCodeError('')
} catch (error) {
console.log("ResetPassword verify error", error?.message);
console.log('ResetPassword verify error', error?.message)
const message =
error?.code && error.code.startsWith("auth/")
error?.code && error.code.startsWith('auth/')
? handleFirebaseError(error.code)
: error?.message || INVALID_LINK_MESSAGE;
setCodeError(message || INVALID_LINK_MESSAGE);
setTooltip({ text: message || INVALID_LINK_MESSAGE, type: "error" });
: error?.message || INVALID_LINK_MESSAGE
setCodeError(message || INVALID_LINK_MESSAGE)
setTooltip({ text: message || INVALID_LINK_MESSAGE, type: 'error' })
} finally {
setIsLoading(false);
setIsVerifyingCode(false);
setIsLoading(false)
setIsVerifyingCode(false)
}
};
}
verifyCode();
}, [oobCode, setIsLoading, setTooltip]);
verifyCode()
}, [oobCode, setIsLoading, setTooltip])
useEffect(() => {
if (!password?.length) {
setPasswordError("");
return;
setPasswordError('')
return
}
if (!isPasswordValid) {
setPasswordError(PASSWORD_ERROR_MESSAGE);
setPasswordError(PASSWORD_ERROR_MESSAGE)
} else {
setPasswordError("");
setPasswordError('')
}
}, [password, isPasswordValid]);
}, [password, isPasswordValid])
useEffect(() => {
if (!confirmPassword?.length) {
setConfirmError("");
return;
setConfirmError('')
return
}
if (!doPasswordsMatch) {
setConfirmError(PASSWORD_MISMATCH_MESSAGE);
setConfirmError(PASSWORD_MISMATCH_MESSAGE)
} else {
setConfirmError("");
setConfirmError('')
}
}, [confirmPassword, doPasswordsMatch]);
}, [confirmPassword, doPasswordsMatch])
const handleSubmit = async () => {
if (!isPasswordValid) {
setPasswordError(PASSWORD_ERROR_MESSAGE);
return;
setPasswordError(PASSWORD_ERROR_MESSAGE)
return
}
if (!doPasswordsMatch) {
setConfirmError(PASSWORD_MISMATCH_MESSAGE);
return;
setConfirmError(PASSWORD_MISMATCH_MESSAGE)
return
}
try {
setIsSubmitting(true);
setIsLoading(true);
await firebase.auth().confirmPasswordReset(oobCode, trimmedPassword);
setIsSubmitting(true)
setIsLoading(true)
await firebase.auth().confirmPasswordReset(oobCode, trimmedPassword)
setTooltip({
text: "Ton mot de passe a été mis à jour. Tu peux te connecter.",
type: "success",
});
navigate(Routes.Login);
text: 'Ton mot de passe a été mis à jour. Tu peux te connecter.',
type: 'success',
})
navigate(Routes.Login)
} catch (error) {
console.log("ResetPassword submit error", error?.message);
console.log('ResetPassword submit error', error?.message)
const message =
error?.code && error.code.startsWith("auth/")
error?.code && error.code.startsWith('auth/')
? handleFirebaseError(error.code)
: error?.message || "Impossible de mettre à jour le mot de passe.";
setTooltip({ text: message, type: "error" });
: error?.message || 'Impossible de mettre à jour le mot de passe.'
setTooltip({ text: message, type: 'error' })
} finally {
setIsLoading(false);
setIsSubmitting(false);
setIsLoading(false)
setIsSubmitting(false)
}
};
}
const renderForm = () => (
<>
@@ -222,7 +214,7 @@ const ResetPassword = () => {
) : null}
</View>
</>
);
)
const renderCodeError = () => (
<View style={{ gap: 12 }}>
@@ -245,28 +237,25 @@ const ResetPassword = () => {
{codeError || INVALID_LINK_MESSAGE}
</Text>
</View>
);
)
const handleFallbackNavigation = () => {
navigate(Routes.ForgotPassword);
};
navigate(Routes.ForgotPassword)
}
const buttonDisabled = codeError
? isSubmitting
: isVerifyingCode ||
isSubmitting ||
!isPasswordValid ||
!doPasswordsMatch;
: isVerifyingCode || isSubmitting || !isPasswordValid || !doPasswordsMatch
const buttonLabel = codeError
? "Demander un nouveau lien"
? 'Demander un nouveau lien'
: isSubmitting
? "Mise à jour..."
? 'Mise à jour...'
: isVerifyingCode
? "Vérification..."
: "Mettre à jour le mot de passe";
? 'Vérification...'
: 'Mettre à jour le mot de passe'
const buttonAction = codeError ? handleFallbackNavigation : handleSubmit;
const buttonAction = codeError ? handleFallbackNavigation : handleSubmit
return (
<Page
@@ -282,8 +271,8 @@ const ResetPassword = () => {
<GradientButton
title={buttonLabel}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={buttonAction}
disabled={buttonDisabled}
@@ -292,7 +281,7 @@ const ResetPassword = () => {
</ItemContainer>
</View>
</Page>
);
};
)
}
export default ResetPassword;
export default ResetPassword
+21 -21
View File
@@ -1,28 +1,28 @@
import { View } from "react-native";
import React, { useContext, useEffect } from "reactn";
import { View } from 'react-native'
import React, { useContext, useEffect } from 'reactn'
import firebase, { usersRef } from "../config/firebase";
import { Routes } from "../navigation";
import firebase, { usersRef } from '../config/firebase'
import { Routes } from '../navigation'
import { SplashAnimationContext } from "../providers/SplashAnimationProvider";
import { UserDataContext } from "../providers/UserDataProvider";
import { SplashAnimationContext } from '../providers/SplashAnimationProvider'
import { UserDataContext } from '../providers/UserDataProvider'
export default ({ navigation }) => {
const { setCurrentUserData } = useContext(UserDataContext);
const { setIsFullyLoaded } = useContext(SplashAnimationContext);
const { setCurrentUserData } = useContext(UserDataContext)
const { setIsFullyLoaded } = useContext(SplashAnimationContext)
useEffect(() => {
userRouting();
}, []);
userRouting()
}, [])
const userRouting = async () => {
try {
const uid = firebase.auth().currentUser?.uid;
if (!uid) throw new Error("No user");
const uid = firebase.auth().currentUser?.uid
if (!uid) throw new Error('No user')
const userSnap = await usersRef.doc(uid).get();
const user = userSnap?.data() || {};
if (userSnap.exists) setCurrentUserData(user);
const userSnap = await usersRef.doc(uid).get()
const user = userSnap?.data() || {}
if (userSnap.exists) setCurrentUserData(user)
navigation.reset({
index: 0,
@@ -31,7 +31,7 @@ export default ({ navigation }) => {
name: Routes.BottomTab,
},
],
});
})
} catch (error) {
// Pas d'utilisateur connecté => accueil avec tab bar
navigation.reset({
@@ -41,11 +41,11 @@ export default ({ navigation }) => {
name: Routes.BottomTab,
},
],
});
})
} finally {
setIsFullyLoaded(true);
setIsFullyLoaded(true)
}
};
}
return <View style={{ flex: 1 }}></View>;
};
return <View style={{ flex: 1 }}></View>
}
+20 -23
View File
@@ -1,23 +1,23 @@
import { View } from "react-native";
import React, { useMemo, useState } from "react";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { CHOOSE_GENRE } from "../../data/data";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import ListSelection from "../../components/ListSelection/ListSelection";
import { View } from 'react-native'
import React, { useMemo, useState } from 'react'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { CHOOSE_GENRE } from '../../data/data'
import ItemContainer from '../../components/ItemContainer/ItemContainer'
import ListSelection from '../../components/ListSelection/ListSelection'
const ChooseGenre = ({ selected = [], setSelected }) => {
const [containerLayout, setContainerLayout] = useState(null);
const [containerLayout, setContainerLayout] = useState(null)
const genreOptions = useMemo(() => {
const seen = new Set();
const seen = new Set()
return (CHOOSE_GENRE ?? []).filter((item) => {
const title = item?.title;
if (!title) return true;
const key = title.trim().toLowerCase();
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}, []);
const title = item?.title
if (!title) return true
const key = title.trim().toLowerCase()
if (seen.has(key)) return false
seen.add(key)
return true
})
}, [])
// Selection handled by ListSelection
@@ -27,10 +27,7 @@ const ChooseGenre = ({ selected = [], setSelected }) => {
title="Choisis un genre"
subTitle="Tu peux choisir jusqu’à 2 genres différents"
/>
<View
style={{ flex: 1 }}
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
>
<View style={{ flex: 1 }} onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}>
<ItemContainer height={containerLayout?.height}>
<ListSelection
options={genreOptions}
@@ -51,7 +48,7 @@ const ChooseGenre = ({ selected = [], setSelected }) => {
</ItemContainer>
</View>
</View>
);
};
)
}
export default ChooseGenre;
export default ChooseGenre
+21 -24
View File
@@ -1,15 +1,15 @@
import { View, StyleSheet, Text } from "react-native";
import { MaterialIcons } from "@expo/vector-icons";
import React, { useState } from "react";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { INSTRUMENTS } from "../../data/data";
import ListSelection from "../../components/ListSelection/ListSelection";
import { View, StyleSheet, Text } from 'react-native'
import { MaterialIcons } from '@expo/vector-icons'
import React, { useState } from 'react'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import ItemContainer from '../../components/ItemContainer/ItemContainer'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { INSTRUMENTS } from '../../data/data'
import ListSelection from '../../components/ListSelection/ListSelection'
const ChooseInstruments = ({ selected = [], setSelected }) => {
const [containerLayout, setContainerLayout] = useState(null);
const [containerLayout, setContainerLayout] = useState(null)
// Selection handled by ListSelection
@@ -21,7 +21,7 @@ const ChooseInstruments = ({ selected = [], setSelected }) => {
>
<View
style={{
flexDirection: "row",
flexDirection: 'row',
gap: 6,
backgroundColor: Palette.transparentOrange,
padding: 8,
@@ -40,15 +40,12 @@ const ChooseInstruments = ({ selected = [], setSelected }) => {
color: Palette.white,
}}
>
Attention : Suno peut ajuster ou supprimer certains paramètres
choisis pour maintenir la cohérence de la création musicale.
Attention : Suno peut ajuster ou supprimer certains paramètres choisis pour maintenir la
cohérence de la création musicale.
</Text>
</View>
</CreateLyricsHeader>
<View
style={{ flex: 1 }}
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
>
<View style={{ flex: 1 }} onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}>
<ItemContainer height={containerLayout?.height}>
<ListSelection
options={INSTRUMENTS}
@@ -64,25 +61,25 @@ const ChooseInstruments = ({ selected = [], setSelected }) => {
</ItemContainer>
</View>
</View>
);
};
)
}
export default ChooseInstruments;
export default ChooseInstruments
const styles = StyleSheet.create({
contentContainer: {
flexGrow: 1,
padding: 8,
gap: 10,
backgroundColor: "#FFFFFF00",
backgroundColor: '#FFFFFF00',
paddingBottom: 50,
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
borderRadius: 14,
overflow: "hidden",
shadowColor: "#00000040",
overflow: 'hidden',
shadowColor: '#00000040',
shadowOffset: {
width: 0,
height: 2,
@@ -97,4 +94,4 @@ const styles = StyleSheet.create({
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
+20 -20
View File
@@ -1,21 +1,21 @@
import { View, StyleSheet } from "react-native";
import React from "react";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { RHYTHM } from "../../data/data";
import ListSelection from "../../components/ListSelection/ListSelection";
import { View, StyleSheet } from 'react-native'
import React from 'react'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import ItemContainer from '../../components/ItemContainer/ItemContainer'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { RHYTHM } from '../../data/data'
import ListSelection from '../../components/ListSelection/ListSelection'
const ITEM_HEIGHT = 54;
const CONTENT_GAP = 10;
const CONTENT_PADDING = 8;
const EXTRA_HEADROOM = 80; // extra space keeps the list fully visible without scrolling
const ITEM_HEIGHT = 54
const CONTENT_GAP = 10
const CONTENT_PADDING = 8
const EXTRA_HEADROOM = 80 // extra space keeps the list fully visible without scrolling
const RHYTHM_CONTAINER_HEIGHT =
RHYTHM.length * ITEM_HEIGHT +
Math.max(RHYTHM.length - 1, 0) * CONTENT_GAP +
CONTENT_PADDING * 2 +
EXTRA_HEADROOM;
EXTRA_HEADROOM
const ChooseRhythm = ({ selected, setSelected }) => {
return (
@@ -35,24 +35,24 @@ const ChooseRhythm = ({ selected, setSelected }) => {
</ItemContainer>
</View>
</View>
);
};
)
}
export default ChooseRhythm;
export default ChooseRhythm
const styles = StyleSheet.create({
contentContainer: {
flexGrow: 1,
padding: CONTENT_PADDING,
gap: CONTENT_GAP,
backgroundColor: "#FFFFFF00",
backgroundColor: '#FFFFFF00',
},
itemContainer: {
height: ITEM_HEIGHT,
backgroundColor: Palette.glass,
borderRadius: 14,
overflow: "hidden",
shadowColor: "#00000040",
overflow: 'hidden',
shadowColor: '#00000040',
shadowOffset: {
width: 0,
height: 2,
@@ -67,4 +67,4 @@ const styles = StyleSheet.create({
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
+21 -27
View File
@@ -1,24 +1,24 @@
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { background } from "../../assets";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
import { useUserData } from "../../providers/UserDataProvider";
import { isWeb } from "../../hooks/useLayoutType";
import React, { useEffect, useState } from 'react'
import { View } from 'react-native'
import { background } from '../../assets'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { gutters } from '../../styles'
import { useUserData } from '../../providers/UserDataProvider'
import { isWeb } from '../../hooks/useLayoutType'
const Compose = () => {
const { videos } = useUserData();
const { videos } = useUserData()
const [videoUrl, setVideoUrl] = useState(null);
const [videoUrl, setVideoUrl] = useState(null)
useEffect(() => {
setVideoUrl(isWeb ? videos.malikWeb : videos?.malik);
}, []);
setVideoUrl(isWeb ? videos.malikWeb : videos?.malik)
}, [])
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader
@@ -26,9 +26,7 @@ const Compose = () => {
progress={9}
// logo={icons.musicLandStudio}
/>
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
<View style={{ flex: 1, position: 'relative', justifyContent: 'flex-end' }}>
<View
style={{
paddingBottom: gutters * 2,
@@ -42,13 +40,9 @@ const Compose = () => {
/>
</View>
</View>
<FullscreenIntroVideo
visible={!!videoUrl}
url={videoUrl}
onClose={() => setVideoUrl(null)}
/>
<FullscreenIntroVideo visible={!!videoUrl} url={videoUrl} onClose={() => setVideoUrl(null)} />
</Page>
);
};
)
}
export default Compose;
export default Compose
+167 -197
View File
@@ -1,249 +1,234 @@
import React, { useMemo, useRef, useState } from "react";
import { useRoute } from "@react-navigation/native";
import { Dimensions, Modal, Text, View } from "react-native";
import SwiperFlatList from "react-native-swiper-flatlist";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import AppAlert from "../../components/Alert";
import CreditAmount from "../../components/CreditAmount";
import firebase, { getFunctionsClient } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { normalizeStructureType } from "../../utils/songStructure";
import { openCoinPackModal } from "../../utils/coinPackModal";
import ChooseGenre from "./ChooseGenre";
import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm";
import CustomizeVoice from "./CustomizeVoice";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import React, { useMemo, useRef, useState } from 'react'
import { useRoute } from '@react-navigation/native'
import { Dimensions, Modal, Text, View } from 'react-native'
import SwiperFlatList from 'react-native-swiper-flatlist'
import { background, icons } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppAlert from '../../components/Alert'
import CreditAmount from '../../components/CreditAmount'
import firebase, { getFunctionsClient } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { normalizeStructureType } from '../../utils/songStructure'
import { openCoinPackModal } from '../../utils/coinPackModal'
import ChooseGenre from './ChooseGenre'
import ChooseInstruments from './ChooseInstruments'
import ChooseRhythm from './ChooseRhythm'
import CustomizeVoice from './CustomizeVoice'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const { width } = Dimensions.get("window");
const MUSIC_GENERATION_COIN_COST = 8;
const CONFIRM_MODAL_MAX_WIDTH = 540;
const FUNCTIONS_REGION = "europe-west1";
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
const OPTIONAL_VOICE_CATEGORIES = new Set(["SENSIBILITÉ", "TECHNIQUE"]);
const FIRST_VOICE_STEP_INDEX = 1;
const TOTAL_STEPS = 1 + VOICE_SECTION_TITLES.length + 2; // genre + voices + instruments + rhythm
const LAST_STEP_INDEX = TOTAL_STEPS - 1;
const INSTRUMENT_STEP_INDEX = FIRST_VOICE_STEP_INDEX + VOICE_SECTION_TITLES.length;
const RHYTHM_STEP_INDEX = LAST_STEP_INDEX;
const { width } = Dimensions.get('window')
const MUSIC_GENERATION_COIN_COST = 8
const CONFIRM_MODAL_MAX_WIDTH = 540
const FUNCTIONS_REGION = 'europe-west1'
const VOICE_SECTION_TITLES = ['BASE', 'SENSIBILITÉ', 'TECHNIQUE']
const OPTIONAL_VOICE_CATEGORIES = new Set(['SENSIBILITÉ', 'TECHNIQUE'])
const FIRST_VOICE_STEP_INDEX = 1
const TOTAL_STEPS = 1 + VOICE_SECTION_TITLES.length + 2 // genre + voices + instruments + rhythm
const LAST_STEP_INDEX = TOTAL_STEPS - 1
const INSTRUMENT_STEP_INDEX = FIRST_VOICE_STEP_INDEX + VOICE_SECTION_TITLES.length
const RHYTHM_STEP_INDEX = LAST_STEP_INDEX
const ComposeSong = () => {
const scrollRef = useRef(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(18);
const [containerLayout, setContainerLayout] = useState(null);
const route = useRoute();
const {
selectedProjectId,
selectedProject,
updateProjectData,
currentUserData,
currentUID,
} = useUser();
const scrollRef = useRef(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(18)
const [containerLayout, setContainerLayout] = useState(null)
const route = useRoute()
const { selectedProjectId, selectedProject, updateProjectData, currentUserData, currentUID } =
useUser()
// Selections state
const [genres, setGenres] = useState([]);
const [genres, setGenres] = useState([])
// voice: object keyed by category (e.g., { base: string|null, Sensibilité: string|null, Technique: string|null })
const [voice, setVoice] = useState({});
const [instruments, setInstruments] = useState([]);
const [rhythm, setRhythm] = useState(null);
const [voice, setVoice] = useState({})
const [instruments, setInstruments] = useState([])
const [rhythm, setRhythm] = useState(null)
const [isConfirmVisible, setIsConfirmVisible] = useState(false);
const [isProcessingConfirmation, setIsProcessingConfirmation] =
useState(false);
const isRegenerationFlow = route?.params?.isRegeneration === true;
const [isConfirmVisible, setIsConfirmVisible] = useState(false)
const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false)
const isRegenerationFlow = route?.params?.isRegeneration === true
const coinBalance = useMemo(() => {
const value = currentUserData?.coins;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
const value = currentUserData?.coins
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const parsed = Number(value);
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed;
return parsed
}
}
return 0;
}, [currentUserData?.coins]);
return 0
}, [currentUserData?.coins])
const isStepValid = useMemo(() => {
if (selectedIndex === 0) {
return Array.isArray(genres) && genres.length > 0;
return Array.isArray(genres) && genres.length > 0
}
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX;
const isVoiceStep =
voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length;
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX
const isVoiceStep = voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length
if (isVoiceStep) {
const category = VOICE_SECTION_TITLES[voiceStepIndex];
const category = VOICE_SECTION_TITLES[voiceStepIndex]
if (OPTIONAL_VOICE_CATEGORIES.has(category)) {
return true;
return true
}
return !!(voice && typeof voice === "object" && voice[category]);
return !!(voice && typeof voice === 'object' && voice[category])
}
if (selectedIndex === INSTRUMENT_STEP_INDEX) {
return Array.isArray(instruments) && instruments.length > 0;
return Array.isArray(instruments) && instruments.length > 0
}
if (selectedIndex === RHYTHM_STEP_INDEX) {
return !!rhythm;
return !!rhythm
}
return true;
}, [selectedIndex, genres, voice, instruments, rhythm]);
return true
}, [selectedIndex, genres, voice, instruments, rhythm])
const musicConfig = useMemo(() => {
let lyricsArr = [];
let lyricsArr = []
if (Array.isArray(selectedProject?.lyrics)) {
lyricsArr = selectedProject.lyrics.map((s) => ({
type: normalizeStructureType(s?.type),
lyrics: s?.lyrics || "",
}));
lyrics: s?.lyrics || '',
}))
} else {
const c = selectedProject?.lyrics?.couplet;
const r = selectedProject?.lyrics?.refrain;
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
const c = selectedProject?.lyrics?.couplet
const r = selectedProject?.lyrics?.refrain
if (c) lyricsArr.push({ type: 'couplet', lyrics: c })
if (r) lyricsArr.push({ type: 'refrain', lyrics: r })
}
const voiceArray = Object.entries(voice || {})
.filter(([, v]) => typeof v === "string" && v.trim())
.map(([category, value]) => ({ category, value }));
.filter(([, v]) => typeof v === 'string' && v.trim())
.map(([category, value]) => ({ category, value }))
return {
title: selectedProject?.title || "",
title: selectedProject?.title || '',
lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [],
voice: voiceArray,
instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined,
projectId: selectedProjectId || undefined,
};
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
}
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId])
const persistMusicConfig = async () => {
try {
if (!selectedProjectId) return;
if (!selectedProjectId) return
const payload = {
musicConfig: {
title: musicConfig?.title || "",
title: musicConfig?.title || '',
lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [],
genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [],
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
instruments: Array.isArray(musicConfig?.instruments)
? musicConfig.instruments
: [],
tempo: musicConfig?.tempo || "",
instruments: Array.isArray(musicConfig?.instruments) ? musicConfig.instruments : [],
tempo: musicConfig?.tempo || '',
},
musicStatus: null,
sunoTaskId: firebase.firestore.FieldValue.delete(),
};
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete();
}
await updateProjectData(payload);
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete()
}
await updateProjectData(payload)
} catch (e) {}
};
}
const spendCoinsForGeneration = async () => {
if (!currentUID) {
throw new Error("Utilisateur introuvable. Merci de réessayer.");
throw new Error('Utilisateur introuvable. Merci de réessayer.')
}
try {
const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
const createSongOrder =
functionsClient.httpsCallable("orders-createSongOrder");
const functionsClient = getFunctionsClient(FUNCTIONS_REGION)
const createSongOrder = functionsClient.httpsCallable('orders-createSongOrder')
await createSongOrder({
amount: -MUSIC_GENERATION_COIN_COST,
songId: selectedProjectId || null,
source: "music_generation",
});
source: 'music_generation',
})
} catch (error) {
const message =
typeof error?.message === "string"
? error.message.replace(
/^functions\.https\.HttpsError:\s*/iu,
"",
)
: null;
typeof error?.message === 'string'
? error.message.replace(/^functions\.https\.HttpsError:\s*/iu, '')
: null
throw new Error(
message ||
"Une erreur est survenue lors de la création de la commande de crédits.",
);
message || 'Une erreur est survenue lors de la création de la commande de crédits.'
)
}
};
}
const handleConfirmGeneration = async () => {
if (isProcessingConfirmation) return;
setIsProcessingConfirmation(true);
if (isProcessingConfirmation) return
setIsProcessingConfirmation(true)
try {
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0;
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0
if (availableCoins < MUSIC_GENERATION_COIN_COST) {
setIsConfirmVisible(false);
setIsConfirmVisible(false)
AppAlert(
"Crédits insuffisants",
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer.",
);
openCoinPackModal();
return;
'Crédits insuffisants',
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer."
)
openCoinPackModal()
return
}
await spendCoinsForGeneration();
await persistMusicConfig();
setIsConfirmVisible(false);
navigate(Routes.GeneratingSong, { config: musicConfig });
await spendCoinsForGeneration()
await persistMusicConfig()
setIsConfirmVisible(false)
navigate(Routes.GeneratingSong, { config: musicConfig })
} catch (error) {
setIsConfirmVisible(false);
setIsConfirmVisible(false)
const message =
error?.message ||
"Une erreur est survenue lors du lancement de la génération.";
AppAlert("Impossible de lancer la génération", message);
error?.message || 'Une erreur est survenue lors du lancement de la génération.'
AppAlert('Impossible de lancer la génération', message)
} finally {
setIsProcessingConfirmation(false);
setIsProcessingConfirmation(false)
}
};
}
const onPressNext = () => {
if (selectedIndex === LAST_STEP_INDEX) {
setIsConfirmVisible(true);
return;
setIsConfirmVisible(true)
return
}
setSelectedIndex((prev) => Math.min(prev + 1, LAST_STEP_INDEX));
setProgress((prev) => prev + 9);
setSelectedIndex((prev) => Math.min(prev + 1, LAST_STEP_INDEX))
setProgress((prev) => prev + 9)
scrollRef.current.scrollToIndex({
index: Math.min(selectedIndex + 1, LAST_STEP_INDEX),
animated: true,
});
};
})
}
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex((prev) => Math.max(prev - 1, 0));
setProgress((prev) => Math.max(18, prev - 9));
setSelectedIndex((prev) => Math.max(prev - 1, 0))
setProgress((prev) => Math.max(18, prev - 9))
scrollRef.current.scrollToIndex({
index: Math.max(selectedIndex - 1, 0),
animated: true,
});
})
} else {
goBack();
goBack()
}
};
}
const handleCancelGeneration = () => {
setIsConfirmVisible(false);
navigate(Routes.SongReady);
};
setIsConfirmVisible(false)
navigate(Routes.SongReady)
}
const swiperSlides = React.Children.toArray([
<View
@@ -265,11 +250,7 @@ const ComposeSong = () => {
paddingHorizontal: gutters,
}}
>
<CustomizeVoice
category={section}
selected={voice}
setSelected={setVoice}
/>
<CustomizeVoice category={section} selected={voice} setSelected={setVoice} />
</View>
)),
<View
@@ -280,10 +261,7 @@ const ComposeSong = () => {
paddingHorizontal: gutters,
}}
>
<ChooseInstruments
selected={instruments}
setSelected={setInstruments}
/>
<ChooseInstruments selected={instruments} setSelected={setInstruments} />
</View>,
<View
key="rhythm"
@@ -295,25 +273,17 @@ const ComposeSong = () => {
>
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
</View>,
]);
])
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader
onPressBack={onPressBack}
progress={progress}
logo={icons.musicLandStudio}
/>
<MusicLandHeader onPressBack={onPressBack} progress={progress} logo={icons.musicLandStudio} />
<View style={{ flex: 1, paddingBottom: gutters, gap: 48 }}>
<View
style={{ flex: 1 }}
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
>
<SwiperFlatList
style={{ width, left: -gutters }}
ref={scrollRef}
disableGesture
>
<SwiperFlatList style={{ width, left: -gutters }} ref={scrollRef} disableGesture>
{swiperSlides}
</SwiperFlatList>
</View>
@@ -326,7 +296,7 @@ const ComposeSong = () => {
/>
)}
<GradientButton
title={selectedIndex === LAST_STEP_INDEX ? "Générer" : "Suivant"}
title={selectedIndex === LAST_STEP_INDEX ? 'Générer' : 'Suivant'}
onPress={onPressNext}
disabled={!isStepValid}
/>
@@ -338,36 +308,36 @@ const ComposeSong = () => {
transparent
visible={isConfirmVisible}
onRequestClose={() => {
if (isProcessingConfirmation) return;
setIsConfirmVisible(false);
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: gutters,
paddingVertical: gutters * 1.5,
backgroundColor: "rgba(0, 0, 0, 0.6)",
backgroundColor: 'rgba(0, 0, 0, 0.6)',
}}
>
<CreateLyricsHeader
containerStyle={{
width: "100%",
width: '100%',
maxWidth: CONFIRM_MODAL_MAX_WIDTH,
alignSelf: "center",
alignSelf: 'center',
paddingVertical: 24,
paddingHorizontal: 24,
gap: 24,
}}
>
<View style={{ gap: 30, alignItems: "center" }}>
<View style={{ gap: 30, alignItems: 'center' }}>
<View
style={{
paddingHorizontal: 15,
gap: 12,
alignItems: "center",
alignItems: 'center',
}}
>
<Text
@@ -375,18 +345,18 @@ const ComposeSong = () => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Générer la musique ?
</Text>
<View style={{ alignItems: "center", gap: 8 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -395,7 +365,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Cette action coûte
@@ -406,7 +376,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
iconSize={18}
/>
@@ -416,7 +386,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Souhaites-tu les utiliser pour lancer la génération ?
@@ -424,9 +394,9 @@ const ComposeSong = () => {
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
}}
>
@@ -435,7 +405,7 @@ const ComposeSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Solde disponible :
@@ -446,13 +416,13 @@ const ComposeSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
iconSize={16}
/>
</View>
</View>
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
<View style={{ width: '85%', alignSelf: 'center', gap: 15 }}>
<GradientButton
title="Confirmer"
onPress={handleConfirmGeneration}
@@ -461,8 +431,8 @@ const ComposeSong = () => {
<BorderGradientButton
title="Annuler"
onPress={() => {
if (isProcessingConfirmation) return;
setIsConfirmVisible(false);
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
disabled={isProcessingConfirmation}
/>
@@ -472,7 +442,7 @@ const ComposeSong = () => {
</View>
</Modal>
</Page>
);
};
)
}
export default ComposeSong;
export default ComposeSong
+178 -240
View File
@@ -1,213 +1,167 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useRoute } from "@react-navigation/native";
import {
FlatList,
Modal,
Text,
View,
useWindowDimensions,
} from "react-native";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import AppAlert from "../../components/Alert";
import CreditAmount from "../../components/CreditAmount";
import firebase, { getFunctionsClient } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { normalizeStructureType } from "../../utils/songStructure";
import { openCoinPackModal } from "../../utils/coinPackModal";
import ChooseGenre from "./ChooseGenre";
import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm";
import CustomizeVoice from "./CustomizeVoice";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useRoute } from '@react-navigation/native'
import { FlatList, Modal, Text, View, useWindowDimensions } from 'react-native'
import { background } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppAlert from '../../components/Alert'
import CreditAmount from '../../components/CreditAmount'
import firebase, { getFunctionsClient } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { normalizeStructureType } from '../../utils/songStructure'
import { openCoinPackModal } from '../../utils/coinPackModal'
import ChooseGenre from './ChooseGenre'
import ChooseInstruments from './ChooseInstruments'
import ChooseRhythm from './ChooseRhythm'
import CustomizeVoice from './CustomizeVoice'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const MUSIC_GENERATION_COIN_COST = 8;
const CONFIRM_MODAL_MAX_WIDTH = 540;
const FUNCTIONS_REGION = "europe-west1";
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
const FIRST_VOICE_STEP_INDEX = 1;
const OPTIONAL_VOICE_CATEGORIES = new Set(["SENSIBILITÉ", "TECHNIQUE"]);
const PAGE_WIDTH_RATIO_WEB = 0.6; // keep in sync with Page default width on web
const PAGE_MAX_WIDTH = 1200;
const PAGE_HORIZONTAL_PADDING = gutters * 2;
const MUSIC_GENERATION_COIN_COST = 8
const CONFIRM_MODAL_MAX_WIDTH = 540
const FUNCTIONS_REGION = 'europe-west1'
const VOICE_SECTION_TITLES = ['BASE', 'SENSIBILITÉ', 'TECHNIQUE']
const FIRST_VOICE_STEP_INDEX = 1
const OPTIONAL_VOICE_CATEGORIES = new Set(['SENSIBILITÉ', 'TECHNIQUE'])
const PAGE_WIDTH_RATIO_WEB = 0.6 // keep in sync with Page default width on web
const PAGE_MAX_WIDTH = 1200
const PAGE_HORIZONTAL_PADDING = gutters * 2
const ComposeSong = () => {
const scrollRef = useRef(null);
const { width: windowWidth } = useWindowDimensions();
const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(18);
const [parentLayout, setParentLayout] = useState(null);
const scrollRef = useRef(null)
const { width: windowWidth } = useWindowDimensions()
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(18)
const [parentLayout, setParentLayout] = useState(null)
const estimatedContainerWidth = useMemo(() => {
const safeWindowWidth =
typeof windowWidth === "number" && Number.isFinite(windowWidth)
? windowWidth
: 0;
const estimatedPageWidth = Math.min(
safeWindowWidth * PAGE_WIDTH_RATIO_WEB,
PAGE_MAX_WIDTH
);
const paddedWidth = estimatedPageWidth - PAGE_HORIZONTAL_PADDING;
return Math.max(1, paddedWidth);
}, [windowWidth]);
const containerWidth = parentLayout?.width || estimatedContainerWidth;
const route = useRoute();
const {
selectedProjectId,
selectedProject,
updateProjectData,
currentUserData,
currentUID,
} = useUser();
typeof windowWidth === 'number' && Number.isFinite(windowWidth) ? windowWidth : 0
const estimatedPageWidth = Math.min(safeWindowWidth * PAGE_WIDTH_RATIO_WEB, PAGE_MAX_WIDTH)
const paddedWidth = estimatedPageWidth - PAGE_HORIZONTAL_PADDING
return Math.max(1, paddedWidth)
}, [windowWidth])
const containerWidth = parentLayout?.width || estimatedContainerWidth
const route = useRoute()
const { selectedProjectId, selectedProject, updateProjectData, currentUserData, currentUID } =
useUser()
const [genres, setGenres] = useState([]);
const [voice, setVoice] = useState({});
const [instruments, setInstruments] = useState([]);
const [rhythm, setRhythm] = useState(null);
const [isConfirmVisible, setIsConfirmVisible] = useState(false);
const [isProcessingConfirmation, setIsProcessingConfirmation] =
useState(false);
const isRegenerationFlow = route?.params?.isRegeneration === true;
const [genres, setGenres] = useState([])
const [voice, setVoice] = useState({})
const [instruments, setInstruments] = useState([])
const [rhythm, setRhythm] = useState(null)
const [isConfirmVisible, setIsConfirmVisible] = useState(false)
const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false)
const isRegenerationFlow = route?.params?.isRegeneration === true
const coinBalance = useMemo(() => {
const value = currentUserData?.coins;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
const value = currentUserData?.coins
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const parsed = Number(value);
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed;
return parsed
}
}
return 0;
}, [currentUserData?.coins]);
return 0
}, [currentUserData?.coins])
const steps = useMemo(
() => [
{
key: "genres",
key: 'genres',
render: () => <ChooseGenre selected={genres} setSelected={setGenres} />,
},
...VOICE_SECTION_TITLES.map((section) => ({
key: `voice-${section}`,
render: () => (
<CustomizeVoice
category={section}
selected={voice}
setSelected={setVoice}
/>
),
render: () => <CustomizeVoice category={section} selected={voice} setSelected={setVoice} />,
})),
{
key: "instruments",
render: () => (
<ChooseInstruments
selected={instruments}
setSelected={setInstruments}
/>
),
key: 'instruments',
render: () => <ChooseInstruments selected={instruments} setSelected={setInstruments} />,
},
{
key: "rhythm",
render: () => (
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
),
key: 'rhythm',
render: () => <ChooseRhythm selected={rhythm} setSelected={setRhythm} />,
},
],
[genres, voice, instruments, rhythm]
);
)
const totalSteps = steps.length;
const lastStepIndex = totalSteps - 1;
const instrumentStepIndex = Math.max(lastStepIndex - 1, 0);
const totalSteps = steps.length
const lastStepIndex = totalSteps - 1
const instrumentStepIndex = Math.max(lastStepIndex - 1, 0)
const isStepValid = useMemo(() => {
if (selectedIndex === 0) {
return Array.isArray(genres) && genres.length > 0;
return Array.isArray(genres) && genres.length > 0
}
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX;
const isVoiceStep =
voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length;
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX
const isVoiceStep = voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length
if (isVoiceStep) {
const category = VOICE_SECTION_TITLES[voiceStepIndex];
const category = VOICE_SECTION_TITLES[voiceStepIndex]
if (OPTIONAL_VOICE_CATEGORIES.has(category)) {
return true;
return true
}
return !!(voice && typeof voice === "object" && voice[category]);
return !!(voice && typeof voice === 'object' && voice[category])
}
if (selectedIndex === instrumentStepIndex) {
return Array.isArray(instruments) && instruments.length > 0;
return Array.isArray(instruments) && instruments.length > 0
}
if (selectedIndex === lastStepIndex) {
return !!rhythm;
return !!rhythm
}
return true;
}, [
selectedIndex,
genres,
voice,
instruments,
rhythm,
instrumentStepIndex,
lastStepIndex,
]);
return true
}, [selectedIndex, genres, voice, instruments, rhythm, instrumentStepIndex, lastStepIndex])
const musicConfig = useMemo(() => {
let lyricsArr = [];
let lyricsArr = []
if (Array.isArray(selectedProject?.lyrics)) {
lyricsArr = selectedProject.lyrics.map((s) => ({
type: normalizeStructureType(s?.type),
lyrics: s?.lyrics || "",
}));
lyrics: s?.lyrics || '',
}))
} else {
const c = selectedProject?.lyrics?.couplet;
const r = selectedProject?.lyrics?.refrain;
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
const c = selectedProject?.lyrics?.couplet
const r = selectedProject?.lyrics?.refrain
if (c) lyricsArr.push({ type: 'couplet', lyrics: c })
if (r) lyricsArr.push({ type: 'refrain', lyrics: r })
}
const voiceArray = Object.entries(voice || {})
.filter(([, v]) => typeof v === "string" && v.trim())
.map(([category, value]) => ({ category, value }));
.filter(([, v]) => typeof v === 'string' && v.trim())
.map(([category, value]) => ({ category, value }))
return {
title: selectedProject?.title || "",
title: selectedProject?.title || '',
lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [],
voice: voiceArray,
instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined,
projectId: selectedProjectId || undefined,
};
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
}
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId])
useEffect(() => {
const nextProgress = 18 + selectedIndex * 9;
const nextProgress = 18 + selectedIndex * 9
if (nextProgress !== progress) {
setProgress(nextProgress);
setProgress(nextProgress)
}
try {
scrollRef.current?.scrollToIndex?.({
index: selectedIndex,
animated: true,
});
})
} catch (_) {}
}, [selectedIndex, progress, containerWidth]);
}, [selectedIndex, progress, containerWidth])
const getItemLayout = useCallback(
(_data, index) => ({
@@ -216,101 +170,88 @@ const ComposeSong = () => {
index,
}),
[containerWidth]
);
)
const handleCancelGeneration = () => {
setIsConfirmVisible(false);
navigate(Routes.SongReady);
};
setIsConfirmVisible(false)
navigate(Routes.SongReady)
}
const persistMusicConfig = useCallback(async () => {
try {
if (!selectedProjectId) return;
if (!selectedProjectId) return
const payload = {
musicConfig: {
title: musicConfig?.title || "",
title: musicConfig?.title || '',
lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [],
genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [],
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
instruments: Array.isArray(musicConfig?.instruments)
? musicConfig.instruments
: [],
tempo: musicConfig?.tempo || "",
instruments: Array.isArray(musicConfig?.instruments) ? musicConfig.instruments : [],
tempo: musicConfig?.tempo || '',
},
musicStatus: null,
sunoTaskId: firebase.firestore.FieldValue.delete(),
};
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete();
}
await updateProjectData(payload);
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete()
}
await updateProjectData(payload)
} catch (_error) {}
}, [
isRegenerationFlow,
musicConfig,
selectedProjectId,
updateProjectData,
]);
}, [isRegenerationFlow, musicConfig, selectedProjectId, updateProjectData])
const spendCoinsForGeneration = useCallback(async () => {
if (!currentUID) {
throw new Error("Utilisateur introuvable. Merci de réessayer.");
throw new Error('Utilisateur introuvable. Merci de réessayer.')
}
try {
const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
const createSongOrder =
functionsClient.httpsCallable("orders-createSongOrder");
const functionsClient = getFunctionsClient(FUNCTIONS_REGION)
const createSongOrder = functionsClient.httpsCallable('orders-createSongOrder')
await createSongOrder({
amount: -MUSIC_GENERATION_COIN_COST,
songId: selectedProjectId || null,
source: "music_generation",
});
source: 'music_generation',
})
} catch (error) {
const message =
typeof error?.message === "string"
? error.message.replace(
/^functions\.https\.HttpsError:\s*/iu,
"",
)
: null;
typeof error?.message === 'string'
? error.message.replace(/^functions\.https\.HttpsError:\s*/iu, '')
: null
throw new Error(
message ||
"Une erreur est survenue lors de la création de la commande de crédits.",
);
message || 'Une erreur est survenue lors de la création de la commande de crédits.'
)
}
}, [currentUID, selectedProjectId]);
}, [currentUID, selectedProjectId])
const handleConfirmGeneration = useCallback(async () => {
if (isProcessingConfirmation) return;
setIsProcessingConfirmation(true);
if (isProcessingConfirmation) return
setIsProcessingConfirmation(true)
try {
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0;
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0
if (availableCoins < MUSIC_GENERATION_COIN_COST) {
setIsConfirmVisible(false);
setIsConfirmVisible(false)
AppAlert(
"Crédits insuffisants",
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer.",
);
openCoinPackModal();
return;
'Crédits insuffisants',
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer."
)
openCoinPackModal()
return
}
await spendCoinsForGeneration();
await persistMusicConfig();
setIsConfirmVisible(false);
navigate(Routes.GeneratingSong, { config: musicConfig });
await spendCoinsForGeneration()
await persistMusicConfig()
setIsConfirmVisible(false)
navigate(Routes.GeneratingSong, { config: musicConfig })
} catch (error) {
setIsConfirmVisible(false);
setIsConfirmVisible(false)
const message =
error?.message ||
"Une erreur est survenue lors du lancement de la génération.";
AppAlert("Impossible de lancer la génération", message);
error?.message || 'Une erreur est survenue lors du lancement de la génération.'
AppAlert('Impossible de lancer la génération', message)
} finally {
setIsProcessingConfirmation(false);
setIsProcessingConfirmation(false)
}
}, [
coinBalance,
@@ -318,23 +259,23 @@ const ComposeSong = () => {
musicConfig,
persistMusicConfig,
spendCoinsForGeneration,
]);
])
const onPressNext = () => {
if (selectedIndex === steps.length - 1) {
setIsConfirmVisible(true);
return;
setIsConfirmVisible(true)
return
}
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1));
};
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1))
}
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex((prev) => Math.max(prev - 1, 0));
setSelectedIndex((prev) => Math.max(prev - 1, 0))
} else {
goBack();
goBack()
}
};
}
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
@@ -344,10 +285,7 @@ const ComposeSong = () => {
// logo={icons.musicLandStudio}
/>
<View style={{ flex: 1, paddingBottom: gutters, gap: 48 }}>
<View
style={{ flex: 1 }}
onLayout={(event) => setParentLayout(event.nativeEvent.layout)}
>
<View style={{ flex: 1 }} onLayout={(event) => setParentLayout(event.nativeEvent.layout)}>
<FlatList
ref={scrollRef}
data={steps}
@@ -381,7 +319,7 @@ const ComposeSong = () => {
/>
)}
<GradientButton
title={selectedIndex === steps.length - 1 ? "Générer" : "Suivant"}
title={selectedIndex === steps.length - 1 ? 'Générer' : 'Suivant'}
onPress={onPressNext}
disabled={!isStepValid}
/>
@@ -393,36 +331,36 @@ const ComposeSong = () => {
transparent
visible={isConfirmVisible}
onRequestClose={() => {
if (isProcessingConfirmation) return;
setIsConfirmVisible(false);
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: gutters,
paddingVertical: gutters * 1.5,
backgroundColor: "rgba(0, 0, 0, 0.6)",
backgroundColor: 'rgba(0, 0, 0, 0.6)',
}}
>
<CreateLyricsHeader
containerStyle={{
width: "100%",
width: '100%',
maxWidth: CONFIRM_MODAL_MAX_WIDTH,
alignSelf: "center",
alignSelf: 'center',
paddingVertical: 24,
paddingHorizontal: 24,
gap: 24,
}}
>
<View style={{ gap: 30, alignItems: "center" }}>
<View style={{ gap: 30, alignItems: 'center' }}>
<View
style={{
paddingHorizontal: 15,
gap: 12,
alignItems: "center",
alignItems: 'center',
}}
>
<Text
@@ -430,18 +368,18 @@ const ComposeSong = () => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Générer la musique ?
</Text>
<View style={{ alignItems: "center", gap: 8 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -450,7 +388,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Cette action coûte
@@ -461,7 +399,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
iconSize={18}
/>
@@ -471,7 +409,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Souhaites-tu les utiliser pour lancer la génération ?
@@ -479,9 +417,9 @@ const ComposeSong = () => {
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
}}
>
@@ -490,7 +428,7 @@ const ComposeSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Solde disponible :
@@ -501,13 +439,13 @@ const ComposeSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
iconSize={16}
/>
</View>
</View>
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
<View style={{ width: '85%', alignSelf: 'center', gap: 15 }}>
<GradientButton
title="Confirmer"
onPress={handleConfirmGeneration}
@@ -516,8 +454,8 @@ const ComposeSong = () => {
<BorderGradientButton
title="Annuler"
onPress={() => {
if (isProcessingConfirmation) return;
setIsConfirmVisible(false);
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
disabled={isProcessingConfirmation}
/>
@@ -527,7 +465,7 @@ const ComposeSong = () => {
</View>
</Modal>
</Page>
);
};
)
}
export default ComposeSong;
export default ComposeSong
+96 -113
View File
@@ -1,101 +1,96 @@
import { BlurView } from "expo-blur";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { icons } from "../../assets";
import AppAlert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import ProgressBar from "../../components/ProgressBar";
import firebase, { projectsRef } from "../../config/firebase";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import { BlurView } from 'expo-blur'
import moment from 'moment'
import React, { useEffect, useState } from 'react'
import { Image, Platform, Pressable, Text, View } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { icons } from '../../assets'
import AppAlert from '../../components/Alert'
import GradientButton from '../../components/GradientButton'
import ProgressBar from '../../components/ProgressBar'
import firebase, { projectsRef } from '../../config/firebase'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import Style, { size } from '../../styles/Style'
const CreatingSong = ({ active, config }) => {
const [progress, setProgress] = useState(0);
const [called, setCalled] = useState(false);
const [, setResult] = useState(null);
const { setIsLoading } = useMinuit();
const [musicStatus, setMusicStatus] = useState(null);
const [generationStartAt, setGenerationStartAt] = useState(null);
const progressTimerRef = React.useRef(null);
const { selectedProjectId } = useUser();
const [progress, setProgress] = useState(0)
const [called, setCalled] = useState(false)
const [, setResult] = useState(null)
const { setIsLoading } = useMinuit()
const [musicStatus, setMusicStatus] = useState(null)
const [generationStartAt, setGenerationStartAt] = useState(null)
const progressTimerRef = React.useRef(null)
const { selectedProjectId } = useUser()
// Abonnement au document projet pour suivre le statut et la date de début
useEffect(() => {
if (!config?.projectId) return undefined;
if (!config?.projectId) return undefined
const unsub = firebase
.firestore()
.collection("projects")
.collection('projects')
.doc(config.projectId)
.onSnapshot((doc) => {
const data = doc.data() || {};
setMusicStatus(data?.musicStatus || null);
setGenerationStartAt(data?.generationStartAt || null);
});
const data = doc.data() || {}
setMusicStatus(data?.musicStatus || null)
setGenerationStartAt(data?.generationStartAt || null)
})
return () => {
if (typeof unsub === "function") unsub();
};
}, [config?.projectId]);
if (typeof unsub === 'function') unsub()
}
}, [config?.projectId])
// Progression basée sur 8 minutes max, arrête si statut change avant
useEffect(() => {
const totalMs = 8 * 60 * 1000; // 8 minutes
const totalMs = 8 * 60 * 1000 // 8 minutes
const clearTimer = () => {
if (progressTimerRef.current) {
globalThis.clearInterval(progressTimerRef.current);
progressTimerRef.current = null;
globalThis.clearInterval(progressTimerRef.current)
progressTimerRef.current = null
}
};
}
if (musicStatus && musicStatus !== "GENERATING") {
setProgress(100);
clearTimer();
return () => clearTimer();
if (musicStatus && musicStatus !== 'GENERATING') {
setProgress(100)
clearTimer()
return () => clearTimer()
}
if (!generationStartAt) {
// En attente de la date de début
return () => clearTimer();
return () => clearTimer()
}
const startDate = generationStartAt?.toDate
? generationStartAt.toDate()
: new Date(generationStartAt);
: new Date(generationStartAt)
const update = () => {
const elapsed = moment().diff(moment(startDate));
const pct = Math.max(
0,
Math.min(100, Math.floor((elapsed / totalMs) * 100))
);
setProgress(pct);
const elapsed = moment().diff(moment(startDate))
const pct = Math.max(0, Math.min(100, Math.floor((elapsed / totalMs) * 100)))
setProgress(pct)
if (pct >= 100) {
clearTimer();
clearTimer()
}
};
}
// Initial update + interval chaque seconde
update();
clearTimer();
progressTimerRef.current = globalThis.setInterval(update, 1000);
update()
clearTimer()
progressTimerRef.current = globalThis.setInterval(update, 1000)
return () => clearTimer();
}, [generationStartAt, musicStatus]);
return () => clearTimer()
}, [generationStartAt, musicStatus])
useEffect(() => {
const run = async () => {
try {
setCalled(true);
await setIsLoading(true);
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
setCalled(true)
await setIsLoading(true)
const callable = firebase.functions().httpsCallable('music-generateMusic')
const { data } = await callable({
title: config?.title,
lyrics: config?.lyrics,
@@ -104,81 +99,71 @@ const CreatingSong = ({ active, config }) => {
instruments: config?.instruments,
tempo: config?.tempo,
projectId: config?.projectId,
});
setResult(data);
})
setResult(data)
// Extraire le taskId renvoyé par l'API Suno à travers la Cloud Function
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
const taskId = data?.response?.data?.taskId || data?.response?.data?.task_id
// Si un projectId et un taskId existent, mettre à jour le projet
if (config?.projectId && taskId) {
const projectRef = projectsRef.doc(config.projectId);
const projectRef = projectsRef.doc(config.projectId)
await projectRef.set(
{
sunoTaskId: taskId,
musicStatus: "GENERATING",
musicStatus: 'GENERATING',
musicError: firebase.firestore.FieldValue.delete(),
musicConfig: {
title: config?.title || "",
title: config?.title || '',
lyrics: config?.lyrics || [],
genres: config?.genres || [],
voice: config?.voice || "",
voice: config?.voice || '',
instruments: config?.instruments || [],
tempo: config?.tempo || "",
tempo: config?.tempo || '',
},
generationStartAt:
firebase.firestore.FieldValue.serverTimestamp(),
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
)
}
} catch (e) {
console.log("CreatingSong generation error", e?.message);
const rawMessage = e?.message || "";
console.log('CreatingSong generation error', e?.message)
const rawMessage = e?.message || ''
const sanitized =
typeof rawMessage === "string"
? rawMessage.replace(/^INTERNAL:/i, "").trim()
: "";
typeof rawMessage === 'string' ? rawMessage.replace(/^INTERNAL:/i, '').trim() : ''
const fallbackMessage =
e?.details?.status ||
"Le service est actuellement indisponible. Merci de réessayer.";
AppAlert(
"Impossible de générer la musique veuillez réessayer plus tard."
);
e?.details?.status || 'Le service est actuellement indisponible. Merci de réessayer.'
AppAlert('Impossible de générer la musique veuillez réessayer plus tard.')
if (config?.projectId) {
try {
await projectsRef.doc(config.projectId).set(
{
musicStatus: "FAILED",
musicStatus: 'FAILED',
sunoTaskId: firebase.firestore.FieldValue.delete(),
generationStartAt: firebase.firestore.FieldValue.delete(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
musicError: {
source: "SUNO_API",
source: 'SUNO_API',
message: sanitized || fallbackMessage,
},
},
{ merge: true }
);
)
} catch (firestoreError) {
console.log(
"CreatingSong Firestore update error",
firestoreError?.message
);
console.log('CreatingSong Firestore update error', firestoreError?.message)
}
}
} finally {
await setIsLoading(false);
await setIsLoading(false)
}
};
}
if (active && !called) {
run();
run()
}
}, [active, called, config, setIsLoading]);
}, [active, called, config, setIsLoading])
return (
<View
@@ -190,20 +175,20 @@ const CreatingSong = ({ active, config }) => {
>
<View
style={{
width: "100%",
height: "50%",
width: '100%',
height: '50%',
}}
>
<View
style={{
flex: 1,
borderRadius: 20,
overflow: "hidden",
backgroundColor: "#0F0C1933",
overflow: 'hidden',
backgroundColor: '#0F0C1933',
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
intensity={Platform.OS !== 'ios' ? 10 : 40}
tint="dark"
style={{ flex: 1, padding: 10, paddingBottom: 20 }}
// experimentalBlurMethod={
@@ -214,7 +199,7 @@ const CreatingSong = ({ active, config }) => {
style={{
...size({ size: 24 }),
...Style.containerCenter,
position: "absolute",
position: 'absolute',
top: 10,
left: 10,
zIndex: 3,
@@ -223,18 +208,18 @@ const CreatingSong = ({ active, config }) => {
>
<Image source={icons.close} style={size({ size: 11 })} />
</Pressable>
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
<View style={{ flex: 1, justifyContent: 'flex-end', gap: 20 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Ta musique est{"\n"}en cours de création
Ta musique est{'\n'}en cours de création
</Text>
<View style={{ alignItems: "center", gap: 16 }}>
<View style={{ alignItems: 'center', gap: 16 }}>
<ProgressBar gradient progress={progress} />
<Text
style={{
@@ -248,14 +233,12 @@ const CreatingSong = ({ active, config }) => {
</View>
<GradientButton
title={
musicStatus === "GENERATING"
? "Création en cours..."
: "Découvrir ma musique"
musicStatus === 'GENERATING' ? 'Création en cours...' : 'Découvrir ma musique'
}
disabled={musicStatus === "GENERATING"}
disabled={musicStatus === 'GENERATING'}
containerStyle={{
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
}}
onPress={() => navigate(Routes.SongReady)}
/>
@@ -264,7 +247,7 @@ const CreatingSong = ({ active, config }) => {
</View>
</View>
</View>
);
};
)
}
export default CreatingSong;
export default CreatingSong
+67 -88
View File
@@ -1,123 +1,102 @@
import { View, StyleSheet } from "react-native";
import React, { useMemo, useState } from "react";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { Palette } from "../../styles";
import { VOICE } from "../../data/data";
import ListSelection from "../../components/ListSelection/ListSelection";
import { View, StyleSheet } from 'react-native'
import React, { useMemo, useState } from 'react'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import ItemContainer from '../../components/ItemContainer/ItemContainer'
import { Palette } from '../../styles'
import { VOICE } from '../../data/data'
import ListSelection from '../../components/ListSelection/ListSelection'
const SECTION_INSTRUCTIONS = {
BASE: "Choisis ta base",
SENSIBILITE: "Choisis ta sensibilité",
TECHNIQUE: "Choisis ta technique",
};
BASE: 'Choisis ta base',
SENSIBILITE: 'Choisis ta sensibilité',
TECHNIQUE: 'Choisis ta technique',
}
const OPTIONAL_SECTION_CATEGORIES = new Set(["SENSIBILITE", "TECHNIQUE"]);
const OPTIONAL_SECTION_CATEGORIES = new Set(['SENSIBILITE', 'TECHNIQUE'])
const normalizeCategory = (value) => {
if (typeof value !== "string") return "";
if (typeof value !== 'string') return ''
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toUpperCase();
};
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toUpperCase()
}
const selectionToObject = (value) => {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value;
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value
}
if (Array.isArray(value)) {
return value.reduce((acc, entry) => {
if (entry && typeof entry === "object") {
const cat = entry.category || entry.title || entry.type;
if (!cat) return acc;
if (entry && typeof entry === 'object') {
const cat = entry.category || entry.title || entry.type
if (!cat) return acc
const val =
entry.value ??
entry.label ??
entry.name ??
(typeof entry.description === "string"
? entry.description
: undefined);
if (typeof val === "string") {
acc[cat] = val;
(typeof entry.description === 'string' ? entry.description : undefined)
if (typeof val === 'string') {
acc[cat] = val
}
return acc;
return acc
}
return acc;
}, {});
return acc
}, {})
}
return {};
};
return {}
}
const objectToSelectionShape = (value, preferArray = false) => {
if (!preferArray) return value;
if (!preferArray) return value
return Object.entries(value || {}).map(([category, val]) => ({
category,
value: val,
}));
};
}))
}
const CustomizeVoice = ({
selected = {},
setSelected = () => {},
category = "BASE",
}) => {
const [containerLayout, setContainerLayout] = useState(null);
const normalizedCategory = normalizeCategory(category);
const CustomizeVoice = ({ selected = {}, setSelected = () => {}, category = 'BASE' }) => {
const [containerLayout, setContainerLayout] = useState(null)
const normalizedCategory = normalizeCategory(category)
const sectionData = useMemo(() => {
const fallback = Array.isArray(VOICE) && VOICE.length > 0 ? VOICE[0] : null;
if (!Array.isArray(VOICE)) return fallback;
const fallback = Array.isArray(VOICE) && VOICE.length > 0 ? VOICE[0] : null
if (!Array.isArray(VOICE)) return fallback
const match =
VOICE.find(
(section) => normalizeCategory(section?.title) === normalizedCategory
) || fallback;
return match;
}, [normalizedCategory]);
VOICE.find((section) => normalizeCategory(section?.title) === normalizedCategory) || fallback
return match
}, [normalizedCategory])
const subtitle =
SECTION_INSTRUCTIONS[normalizedCategory] ||
"Choisis la voix pour ta chanson";
const isOptionalSection = OPTIONAL_SECTION_CATEGORIES.has(normalizedCategory);
const subtitle = SECTION_INSTRUCTIONS[normalizedCategory] || 'Choisis la voix pour ta chanson'
const isOptionalSection = OPTIONAL_SECTION_CATEGORIES.has(normalizedCategory)
const title = `Personnalise la voix que tu veux pour ta chanson${
isOptionalSection ? " (Facultatif)" : ""
}`;
isOptionalSection ? ' (Facultatif)' : ''
}`
const voiceObject = useMemo(
() => selectionToObject(selected),
[selected]
);
const sectionKey = sectionData?.title || "BASE";
const voiceObject = useMemo(() => selectionToObject(selected), [selected])
const sectionKey = sectionData?.title || 'BASE'
const currentValue =
typeof voiceObject?.[sectionKey] === "string"
? voiceObject[sectionKey]
: null;
typeof voiceObject?.[sectionKey] === 'string' ? voiceObject[sectionKey] : null
const handleSelectionChange = (value) => {
if (typeof setSelected !== "function") return;
if (typeof setSelected !== 'function') return
setSelected((previous) => {
const wasArray = Array.isArray(previous);
const baseObject = selectionToObject(previous);
const nextObject = { ...baseObject };
if (typeof value === "string" && value.length > 0) {
nextObject[sectionKey] = value;
const wasArray = Array.isArray(previous)
const baseObject = selectionToObject(previous)
const nextObject = { ...baseObject }
if (typeof value === 'string' && value.length > 0) {
nextObject[sectionKey] = value
} else {
delete nextObject[sectionKey];
delete nextObject[sectionKey]
}
return objectToSelectionShape(nextObject, wasArray);
});
};
return objectToSelectionShape(nextObject, wasArray)
})
}
return (
<View style={{ flex: 1, gap: 10, paddingTop: 16 }}>
<CreateLyricsHeader
title={title}
subTitle={subtitle}
/>
<View
style={{ flex: 1 }}
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
>
<CreateLyricsHeader title={title} subTitle={subtitle} />
<View style={{ flex: 1 }} onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}>
<ItemContainer height={containerLayout?.height}>
<View style={{ flex: 1, gap: 10 }}>
<ListSelection
@@ -132,25 +111,25 @@ const CustomizeVoice = ({
</ItemContainer>
</View>
</View>
);
};
)
}
export default CustomizeVoice;
export default CustomizeVoice
const styles = StyleSheet.create({
contentContainer: {
flexGrow: 1,
padding: 8,
gap: 10,
backgroundColor: "#FFFFFF00",
backgroundColor: '#FFFFFF00',
paddingBottom: 50,
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
borderRadius: 14,
overflow: "hidden",
shadowColor: "#00000040",
overflow: 'hidden',
shadowColor: '#00000040',
shadowOffset: {
width: 0,
height: 2,
@@ -161,4 +140,4 @@ const styles = StyleSheet.create({
elevation: 5,
},
// itemText was used in legacy implementation; kept here for consistency if needed later
});
})
+175 -212
View File
@@ -1,168 +1,161 @@
import { useIsFocused, useNavigation, StackActions } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import moment from "moment";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Platform, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import AppAlert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import ProgressBar from "../../components/ProgressBar";
import firebase, { projectsRef } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { useIsFocused, useNavigation, StackActions } from '@react-navigation/native'
import { BlurView } from 'expo-blur'
import moment from 'moment'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Platform, Text, View } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import AppAlert from '../../components/Alert'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import ProgressBar from '../../components/ProgressBar'
import firebase, { projectsRef } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const GeneratingSong = () => {
const navigation = useNavigation();
const { selectedProjectId, selectedProject } = useUser();
const [progress, setProgress] = useState(0);
const [isGenerationInFlight, setIsGenerationInFlight] = useState(false);
const { setIsLoading } = useMinuit();
const progressTimerRef = useRef(null);
const navigatedRef = useRef(false);
const isFocused = useIsFocused();
const askedRef = useRef(false);
const callingRef = useRef(false);
const localStartRef = useRef(null);
const errorAlertShownRef = useRef(false);
const navigation = useNavigation()
const { selectedProjectId, selectedProject } = useUser()
const [progress, setProgress] = useState(0)
const [isGenerationInFlight, setIsGenerationInFlight] = useState(false)
const { setIsLoading } = useMinuit()
const progressTimerRef = useRef(null)
const navigatedRef = useRef(false)
const isFocused = useIsFocused()
const askedRef = useRef(false)
const callingRef = useRef(false)
const localStartRef = useRef(null)
const errorAlertShownRef = useRef(false)
const isFailed = selectedProject?.musicStatus === "FAILED";
const isFailed = selectedProject?.musicStatus === 'FAILED'
const musicErrorMessage =
selectedProject?.musicError?.message ||
selectedProject?.musicError?.status ||
"Une erreur est survenue lors de la génération.";
'Une erreur est survenue lors de la génération.'
const hasRefund =
selectedProject?.musicCreditsRefunded === true ||
typeof selectedProject?.musicCreditsRefundOrderId === "string";
typeof selectedProject?.musicCreditsRefundOrderId === 'string'
// project loaded from provider
// Progress based on an 8 minute cap or until status becomes GENERATED
useEffect(() => {
const totalMs = 8 * 60 * 1000;
const maxGeneratingProgress = 90;
const totalMs = 8 * 60 * 1000
const maxGeneratingProgress = 90
const clearTimer = () => {
if (progressTimerRef.current) {
global.clearInterval(progressTimerRef.current);
progressTimerRef.current = null;
global.clearInterval(progressTimerRef.current)
progressTimerRef.current = null
}
};
}
const status = selectedProject?.musicStatus;
const status = selectedProject?.musicStatus
const readyUrls = Array.isArray(selectedProject?.musicUrls)
? selectedProject.musicUrls.filter(Boolean)
: [];
const hasReadySong = readyUrls.length > 0 || !!selectedProject?.songUrl;
: []
const hasReadySong = readyUrls.length > 0 || !!selectedProject?.songUrl
const isActivelyGenerating =
status === "GENERATING" ||
(isGenerationInFlight &&
status !== "FAILED" &&
status !== "GENERATED");
status === 'GENERATING' ||
(isGenerationInFlight && status !== 'FAILED' && status !== 'GENERATED')
if (!isActivelyGenerating) {
// Reset local start when leaving generating state
localStartRef.current = null;
localStartRef.current = null
}
if (status === "GENERATED") {
clearTimer();
setIsGenerationInFlight(false);
setProgress(hasReadySong ? 100 : maxGeneratingProgress);
return () => clearTimer();
if (status === 'GENERATED') {
clearTimer()
setIsGenerationInFlight(false)
setProgress(hasReadySong ? 100 : maxGeneratingProgress)
return () => clearTimer()
}
if (!isActivelyGenerating) {
clearTimer();
if (status !== "FAILED") {
setProgress(0);
clearTimer()
if (status !== 'FAILED') {
setProgress(0)
}
return () => clearTimer();
return () => clearTimer()
}
const resolveStartDate = () => {
const gs = selectedProject?.generationStartAt;
if (gs?.toDate) return gs.toDate();
if (gs) return new Date(gs);
if (!localStartRef.current) localStartRef.current = new Date();
return localStartRef.current;
};
const gs = selectedProject?.generationStartAt
if (gs?.toDate) return gs.toDate()
if (gs) return new Date(gs)
if (!localStartRef.current) localStartRef.current = new Date()
return localStartRef.current
}
const update = () => {
const startDate = resolveStartDate();
const startDate = resolveStartDate()
if (!startDate) {
setProgress(0);
return;
setProgress(0)
return
}
const elapsed = moment().diff(moment(startDate));
const raw = Math.floor((elapsed / totalMs) * 100);
const elapsed = moment().diff(moment(startDate))
const raw = Math.floor((elapsed / totalMs) * 100)
// While status is GENERATING, block visual progress at 90%
const pct = Math.max(1, Math.min(maxGeneratingProgress, raw));
setProgress(pct);
};
const pct = Math.max(1, Math.min(maxGeneratingProgress, raw))
setProgress(pct)
}
update();
clearTimer();
progressTimerRef.current = global.setInterval(update, 1000);
return () => clearTimer();
update()
clearTimer()
progressTimerRef.current = global.setInterval(update, 1000)
return () => clearTimer()
}, [
isGenerationInFlight,
selectedProject?.musicStatus,
selectedProject?.generationStartAt,
selectedProject?.musicUrls,
selectedProject?.songUrl,
]);
])
// Auto navigate to SongReady when generation completed
useEffect(() => {
if (!selectedProjectId) return;
if (selectedProject?.musicStatus === "GENERATED" && !navigatedRef.current) {
navigatedRef.current = true;
navigation.dispatch(StackActions.replace(Routes.SongReady));
if (!selectedProjectId) return
if (selectedProject?.musicStatus === 'GENERATED' && !navigatedRef.current) {
navigatedRef.current = true
navigation.dispatch(StackActions.replace(Routes.SongReady))
}
}, [selectedProject?.musicStatus, selectedProjectId, navigation]);
}, [selectedProject?.musicStatus, selectedProjectId, navigation])
useEffect(() => {
if (
selectedProject?.musicStatus === "GENERATED" ||
selectedProject?.musicStatus === "FAILED"
) {
setIsGenerationInFlight(false);
if (selectedProject?.musicStatus === 'GENERATED' || selectedProject?.musicStatus === 'FAILED') {
setIsGenerationInFlight(false)
}
}, [selectedProject?.musicStatus]);
}, [selectedProject?.musicStatus])
const effectiveConfig = useMemo(() => {
// Use selectedProject.musicConfig only
const cfg = selectedProject?.musicConfig || {};
const cfg = selectedProject?.musicConfig || {}
return {
title: cfg?.title || "",
title: cfg?.title || '',
lyrics: Array.isArray(cfg?.lyrics) ? cfg.lyrics : [],
genres: Array.isArray(cfg?.genres) ? cfg.genres : [],
voice: Array.isArray(cfg?.voice) ? cfg.voice : [],
instruments: Array.isArray(cfg?.instruments) ? cfg.instruments : [],
tempo: cfg?.tempo || "",
};
}, [selectedProject?.musicConfig]);
tempo: cfg?.tempo || '',
}
}, [selectedProject?.musicConfig])
async function startMusicGeneration() {
try {
console.log("startMusicGeneration");
setIsGenerationInFlight(true);
console.log('startMusicGeneration')
setIsGenerationInFlight(true)
if (!localStartRef.current) {
localStartRef.current = new Date();
localStartRef.current = new Date()
}
setProgress(1);
await setIsLoading(true);
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
const cfg = effectiveConfig || {};
setProgress(1)
await setIsLoading(true)
const callable = firebase.functions().httpsCallable('music-generateMusic')
const cfg = effectiveConfig || {}
const { data } = await callable({
title: cfg?.title,
lyrics: cfg?.lyrics,
@@ -171,148 +164,122 @@ const GeneratingSong = () => {
instruments: cfg?.instruments,
tempo: cfg?.tempo,
projectId: selectedProjectId || selectedProject?.id || null,
});
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
})
const taskId = data?.response?.data?.taskId || data?.response?.data?.task_id
if (selectedProjectId && taskId) {
const baseUpdate = {
sunoTaskId: taskId,
musicStatus: "GENERATING",
musicStatus: 'GENERATING',
musicError: firebase.firestore.FieldValue.delete(),
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
};
}
const updatePayload = selectedProject?.musicConfig
? baseUpdate
: {
...baseUpdate,
musicConfig: {
title: effectiveConfig?.title || "",
lyrics: effectiveConfig?.lyrics || [],
genres: effectiveConfig?.genres || [],
voice: effectiveConfig?.voice || "",
instruments: effectiveConfig?.instruments || [],
tempo: effectiveConfig?.tempo || "",
},
};
await projectsRef
.doc(selectedProjectId)
.set(updatePayload, { merge: true });
...baseUpdate,
musicConfig: {
title: effectiveConfig?.title || '',
lyrics: effectiveConfig?.lyrics || [],
genres: effectiveConfig?.genres || [],
voice: effectiveConfig?.voice || '',
instruments: effectiveConfig?.instruments || [],
tempo: effectiveConfig?.tempo || '',
},
}
await projectsRef.doc(selectedProjectId).set(updatePayload, { merge: true })
}
} catch (e) {
console.log("GeneratingSong error", e?.message);
const rawMessage = e?.message || "";
console.log('GeneratingSong error', e?.message)
const rawMessage = e?.message || ''
const sanitized =
typeof rawMessage === "string"
? rawMessage.replace(/^INTERNAL:/i, "").trim()
: "";
typeof rawMessage === 'string' ? rawMessage.replace(/^INTERNAL:/i, '').trim() : ''
const fallbackMessage =
e?.details?.message ||
e?.details?.status ||
"Le service Suno est indisponible. Veuillez réessayer plus tard.";
AppAlert(
"Impossible de générer la musique",
sanitized || fallbackMessage,
);
'Le service Suno est indisponible. Veuillez réessayer plus tard.'
AppAlert('Impossible de générer la musique', sanitized || fallbackMessage)
if (selectedProjectId) {
try {
await projectsRef.doc(selectedProjectId).set(
{
musicStatus: "FAILED",
musicStatus: 'FAILED',
sunoTaskId: firebase.firestore.FieldValue.delete(),
generationStartAt: firebase.firestore.FieldValue.delete(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
musicError: {
source: "SUNO_API",
source: 'SUNO_API',
message: sanitized || fallbackMessage,
},
},
{ merge: true },
);
{ merge: true }
)
} catch (firestoreError) {
console.log(
"GeneratingSong Firestore update error",
firestoreError?.message,
);
console.log('GeneratingSong Firestore update error', firestoreError?.message)
}
}
askedRef.current = false;
setIsGenerationInFlight(false);
askedRef.current = false
setIsGenerationInFlight(false)
} finally {
await setIsLoading(false);
await setIsLoading(false)
}
}
// Ensure we never call generation more than once concurrently
const startMusicGenerationOnce = React.useCallback(() => {
if (callingRef.current) return;
callingRef.current = true;
if (callingRef.current) return
callingRef.current = true
startMusicGeneration()
.catch(() => { })
.catch(() => {})
.finally(() => {
// Keep locked while status transitions to GENERATING; will be prevented by guards
global.setTimeout(() => {
callingRef.current = false;
}, 500);
});
}, [startMusicGeneration]);
callingRef.current = false
}, 500)
})
}, [startMusicGeneration])
// Trigger generation only when focused; ask once while idle
useEffect(() => {
if (!isFocused) return;
const status = selectedProject?.musicStatus;
const titleOk = !!selectedProject?.title;
if (!isFocused) return
const status = selectedProject?.musicStatus
const titleOk = !!selectedProject?.title
// Idle if not actively generating or generated (ignore stale taskId)
const isIdle =
status == null ||
status === "" ||
status === "PENDING" ||
status === "READY" ||
status === "FAILED";
status === '' ||
status === 'PENDING' ||
status === 'READY' ||
status === 'FAILED'
if (titleOk && isIdle && !askedRef.current) {
askedRef.current = true;
startMusicGenerationOnce();
askedRef.current = true
startMusicGenerationOnce()
}
}, [
isFocused,
selectedProject?.title,
selectedProject?.musicStatus,
startMusicGenerationOnce,
]);
}, [isFocused, selectedProject?.title, selectedProject?.musicStatus, startMusicGenerationOnce])
useEffect(() => {
if (isFailed) {
if (!errorAlertShownRef.current) {
errorAlertShownRef.current = true;
const refundNotice = hasRefund
? "\n\nTes crédits ont été automatiquement remboursés."
: "";
AppAlert("Génération échouée", `${musicErrorMessage}${refundNotice}`);
errorAlertShownRef.current = true
const refundNotice = hasRefund ? '\n\nTes crédits ont été automatiquement remboursés.' : ''
AppAlert('Génération échouée', `${musicErrorMessage}${refundNotice}`)
}
} else if (selectedProject?.musicStatus === "GENERATING") {
errorAlertShownRef.current = false;
} else if (selectedProject?.musicStatus === 'GENERATING') {
errorAlertShownRef.current = false
}
}, [
hasRefund,
isFailed,
musicErrorMessage,
selectedProject?.musicStatus,
]);
}, [hasRefund, isFailed, musicErrorMessage, selectedProject?.musicStatus])
const progressStatus = isFailed ? "error" : "default";
const displayProgress = Math.max(
0,
Math.min(100, Math.round(progress || 0)),
);
const progressLabel = isFailed ? "Erreur" : `${displayProgress}%`;
const progressStatus = isFailed ? 'error' : 'default'
const displayProgress = Math.max(0, Math.min(100, Math.round(progress || 0)))
const progressLabel = isFailed ? 'Erreur' : `${displayProgress}%`
return (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader
onPressBack={() => navigate(Routes.Home)}
progress={63}
// logo={icons.musicLandStudio}
// logo={icons.musicLandStudio}
/>
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
@@ -328,45 +295,41 @@ const GeneratingSong = () => {
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
intensity={Platform.OS !== 'ios' ? 10 : 40}
tint="dark"
style={{
flex: 1,
borderRadius: 16,
overflow: "hidden",
overflow: 'hidden',
padding: 12,
backgroundColor: "#FFFFFF0A",
backgroundColor: '#FFFFFF0A',
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ flex: 1 }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
intensity={Platform.OS !== 'ios' ? 10 : 40}
tint="dark"
style={{ flex: 1, padding: 10, paddingBottom: 20 }}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
<View style={{ flex: 1, justifyContent: 'flex-end', gap: 20 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Ta musique est{"\n"}en cours de création
Ta musique est{'\n'}en cours de création
</Text>
<View style={{ alignItems: "center", gap: 16 }}>
<ProgressBar
gradient
progress={displayProgress}
status={progressStatus}
/>
<View style={{ alignItems: 'center', gap: 16 }}>
<ProgressBar gradient progress={displayProgress} status={progressStatus} />
<Text
style={{
fontSize: 14,
@@ -383,24 +346,24 @@ const GeneratingSong = () => {
fontSize: 13,
color: Palette.red,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
lineHeight: 20,
}}
>
{musicErrorMessage}
{hasRefund
? "\nTes crédits ont été remboursés automatiquement."
: "\nTu peux revenir en arrière pour relancer la génération."}
? '\nTes crédits ont été remboursés automatiquement.'
: '\nTu peux revenir en arrière pour relancer la génération.'}
</Text>
) : null}
<GradientButton
title={
selectedProject?.musicStatus === "GENERATED"
? "Découvrir ma musique"
: "Création en cours..."
selectedProject?.musicStatus === 'GENERATED'
? 'Découvrir ma musique'
: 'Création en cours...'
}
disabled={selectedProject?.musicStatus !== "GENERATED"}
containerStyle={{ width: "80%", alignSelf: "center" }}
disabled={selectedProject?.musicStatus !== 'GENERATED'}
containerStyle={{ width: '80%', alignSelf: 'center' }}
onPress={() => navigate(Routes.SongReady)}
/>
</View>
@@ -410,7 +373,7 @@ const GeneratingSong = () => {
</View>
</View>
</Page>
);
};
)
}
export default GeneratingSong;
export default GeneratingSong
+24 -33
View File
@@ -1,17 +1,17 @@
import { View, Text } from "react-native";
import React from "react";
import Page from "../../layouts/Page";
import { background, icons } from "../../assets";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import GradientButton from "../../components/GradientButton";
import { useRoute } from "@react-navigation/native";
import { View, Text } from 'react-native'
import React from 'react'
import Page from '../../layouts/Page'
import { background, icons } from '../../assets'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import MusicLandHeader from '../../components/MusicLandHeader'
import { goBack } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import GradientButton from '../../components/GradientButton'
import { useRoute } from '@react-navigation/native'
const Regenerate = () => {
const params = useRoute().params;
const params = useRoute().params
return (
<Page
@@ -21,10 +21,7 @@ const Regenerate = () => {
>
<MusicLandHeader onPressBack={goBack} progress={params?.progress} />
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Regénérer"
subTitle="Veux-tu changer tes choix ?"
/>
<CreateLyricsHeader title="Regénérer" subTitle="Veux-tu changer tes choix ?" />
<View style={{ gap: 27, marginTop: 30 }}>
<CreateLyricsHeader title="Genre">
<Text
@@ -34,18 +31,12 @@ const Regenerate = () => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>
Hip Hop/Rap
</Text>{" "}
: Musique commerciale destinée au grand public, accrocheuse et
mélodique. Domine les charts internationaux avec des artistes
ultra-médiatisés.
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>Hip Hop/Rap</Text> : Musique
commerciale destinée au grand public, accrocheuse et mélodique. Domine les charts
internationaux avec des artistes ultra-médiatisés.
</Text>
</CreateLyricsHeader>
<CreateLyricsHeader
title="Voix"
subTitle="Une voix masculine interpretera ta chanson."
/>
<CreateLyricsHeader title="Voix" subTitle="Une voix masculine interpretera ta chanson." />
<CreateLyricsHeader title="Instruments">
<Text
style={{
@@ -54,21 +45,21 @@ const Regenerate = () => {
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Banjo{"\n"}Violon{"\n"}
Saxophone{"\n"}
Piano classique{"\n"}
Banjo{'\n'}Violon{'\n'}
Saxophone{'\n'}
Piano classique{'\n'}
</Text>
</CreateLyricsHeader>
</View>
</View>
<GradientButton
title="Regénérer"
containerStyle={{ width: "80%", alignSelf: "center" }}
containerStyle={{ width: '80%', alignSelf: 'center' }}
icon={icons.stars}
onPress={goBack}
/>
</Page>
);
};
)
}
export default Regenerate;
export default Regenerate
+228 -254
View File
@@ -1,7 +1,7 @@
/* global setInterval, clearInterval */
import { useFocusEffect } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useFocusEffect } from '@react-navigation/native'
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
FlatList,
Image,
@@ -11,30 +11,30 @@ import {
Text,
View,
useWindowDimensions,
} from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import alert from "../../components/Alert";
import BorderGradientButton from "../../components/BorderGradientButton";
import CreditAmount from "../../components/CreditAmount";
import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader";
import ProgressSlider from "../../components/player/ProgressSlider";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate, reset } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style } from "../../styles";
import { gutters, size } from "../../styles/Style";
import { FONT_FAMILY } from "../../styles/Fonts";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
} from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background, icons } from '../../assets'
import alert from '../../components/Alert'
import BorderGradientButton from '../../components/BorderGradientButton'
import CreditAmount from '../../components/CreditAmount'
import GradientButton from '../../components/GradientButton'
import ValidateModal from '../../components/modal/ValidateModal'
import MusicLandHeader from '../../components/MusicLandHeader'
import ProgressSlider from '../../components/player/ProgressSlider'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate, reset } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette, Style } from '../../styles'
import { gutters, size } from '../../styles/Style'
import { FONT_FAMILY } from '../../styles/Fonts'
import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const MUSIC_GENERATION_COIN_COST = 8;
const SONG_OPTIONS_PER_GENERATION = 2;
const REGENERATE_MODAL_MAX_WIDTH = 540;
const MUSIC_GENERATION_COIN_COST = 8
const SONG_OPTIONS_PER_GENERATION = 2
const REGENERATE_MODAL_MAX_WIDTH = 540
const SongReady = () => {
const {
@@ -42,81 +42,79 @@ const SongReady = () => {
selectedProject,
updateProjectData,
updateUserData,
} = useUser();
const [showValidateModal, setShowValidateModal] = useState(false);
const [showRegenerateModal, setShowRegenerateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const playerRefs = useRef({});
} = useUser()
const [showValidateModal, setShowValidateModal] = useState(false)
const [showRegenerateModal, setShowRegenerateModal] = useState(false)
const [musicUrls, setMusicUrls] = useState([])
const [selectedIndex, setSelectedIndex] = useState(0)
const playerRefs = useRef({})
const registerPlayer = useCallback((index, player) => {
if (player) {
playerRefs.current[index] = player;
playerRefs.current[index] = player
} else {
delete playerRefs.current[index];
delete playerRefs.current[index]
}
}, []);
}, [])
const pauseAllExcept = useCallback(async (keepIndex = null) => {
const tasks = Object.entries(playerRefs.current).map(
async ([key, player]) => {
const idx = Number(key);
if (!player || idx === keepIndex) return;
try {
await player.pause?.();
} catch (error) {
console.log("SongReady pause error", error?.message);
}
},
);
await Promise.all(tasks);
}, []);
const tasks = Object.entries(playerRefs.current).map(async ([key, player]) => {
const idx = Number(key)
if (!player || idx === keepIndex) return
try {
await player.pause?.()
} catch (error) {
console.log('SongReady pause error', error?.message)
}
})
await Promise.all(tasks)
}, [])
const pauseAllPlayers = useCallback(async () => {
await pauseAllExcept(null);
}, [pauseAllExcept]);
await pauseAllExcept(null)
}, [pauseAllExcept])
const handleTogglePlayback = useCallback(
async (index, isPlayingNow) => {
const player = playerRefs.current[index];
if (!player) return;
const player = playerRefs.current[index]
if (!player) return
if (isPlayingNow) {
try {
await player.pause?.();
await player.pause?.()
} catch (error) {
console.log("SongReady pause toggle", error?.message);
console.log('SongReady pause toggle', error?.message)
}
return;
return
}
await pauseAllExcept(index);
await pauseAllExcept(index)
try {
await player.play?.();
await player.play?.()
} catch (error) {
console.log("SongReady play error", error?.message);
console.log('SongReady play error', error?.message)
}
},
[pauseAllExcept],
);
[pauseAllExcept]
)
// Sync URLs from provider's selectedProject
useEffect(() => {
const urls = Array.isArray(selectedProject?.musicUrls)
? selectedProject.musicUrls
.filter((url) => typeof url === "string" && url.trim())
.map((url) => url.trim())
: [];
setMusicUrls(urls);
.filter((url) => typeof url === 'string' && url.trim())
.map((url) => url.trim())
: []
setMusicUrls(urls)
setSelectedIndex((prev) => {
if (!urls.length) return 0;
return Math.min(prev, urls.length - 1);
});
}, [selectedProject?.musicUrls]);
if (!urls.length) return 0
return Math.min(prev, urls.length - 1)
})
}, [selectedProject?.musicUrls])
const validateSelection = async () => {
try {
const url = musicUrls[selectedIndex];
const url = musicUrls[selectedIndex]
if (!projectId || !url) return;
if (!projectId || !url) return
await updateProjectData({
songIndex: selectedIndex,
songUrl: url,
@@ -126,74 +124,74 @@ const SongReady = () => {
playbackUrl: null,
playbackStatus: null,
playbackGenerating: null,
});
})
// Incrémenter le compteur de chansons générées
const currentGeneratedSongs = selectedProject?.user?.generatedSongs || 0;
const currentGeneratedSongs = selectedProject?.user?.generatedSongs || 0
await updateUserData({
data: {
generatedSongs: currentGeneratedSongs + 1,
},
shouldSetTooltip: false,
});
})
await pauseAllPlayers();
await pauseAllPlayers()
reset({
index: 1,
routes: [{ name: Routes.BottomTab }, { name: Routes.ChooseCoverType }],
});
})
} catch (e) {
console.log("Validate error", e?.message);
console.log('Validate error', e?.message)
}
};
}
// Pause audio when screen loses focus (navigate/reset) and on unmount
useFocusEffect(
useCallback(() => {
return () => {
pauseAllPlayers();
};
}, [pauseAllPlayers]),
);
pauseAllPlayers()
}
}, [pauseAllPlayers])
)
useEffect(() => {
return () => {
pauseAllPlayers();
};
}, [pauseAllPlayers]);
pauseAllPlayers()
}
}, [pauseAllPlayers])
const handleChooseTrack = () => {
if (isWeb) {
alert(
"Attention !",
"Lorsque tu clique sur valider, tu ne pourra plus changer ni le texte ni la mélodie.",
'Attention !',
'Lorsque tu clique sur valider, tu ne pourra plus changer ni le texte ni la mélodie.',
[
{
text: "Retour",
style: "cancel",
text: 'Retour',
style: 'cancel',
},
{
text: "Valider",
style: "confirm",
text: 'Valider',
style: 'confirm',
onPress: () => {
validateSelection();
validateSelection()
},
},
],
{ cancelable: false },
);
return;
{ cancelable: false }
)
return
}
setShowValidateModal(true);
};
setShowValidateModal(true)
}
const handleConfirmRegenerate = async () => {
setShowRegenerateModal(false);
setShowRegenerateModal(false)
try {
await pauseAllPlayers();
} catch { }
navigate(Routes.Lyrics);
};
await pauseAllPlayers()
} catch {}
navigate(Routes.Lyrics)
}
const handleRegeneratePress = () => {
if (isWeb) {
@@ -201,19 +199,19 @@ const SongReady = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
};
textAlign: 'center',
}
const amountTextStyle = {
...descriptionTextStyle,
fontFamily: FONT_FAMILY.InterSemiBold,
};
}
alert(
"Re-générer le morceau",
'Re-générer le morceau',
<View
style={{
width: "100%",
alignItems: "center",
width: '100%',
alignItems: 'center',
gap: 12,
}}
>
@@ -222,10 +220,10 @@ const SongReady = () => {
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -242,30 +240,30 @@ const SongReady = () => {
</View>,
[
{
text: "Annuler",
style: "cancel",
text: 'Annuler',
style: 'cancel',
},
{
text: "Oui",
style: "confirm",
text: 'Oui',
style: 'confirm',
onPress: () => handleConfirmRegenerate(),
},
],
{ cancelable: true },
);
return;
{ cancelable: true }
)
return
}
setShowRegenerateModal(true);
};
setShowRegenerateModal(true)
}
return (
<Page headerType="NONE" backgroundImg={background.studioBG2} maxWidth={800}>
<MusicLandHeader
onPressBack={async () => {
try {
await pauseAllPlayers();
} catch { }
navigate(Routes.Home);
await pauseAllPlayers()
} catch {}
navigate(Routes.Home)
}}
progress={63}
/>
@@ -280,7 +278,7 @@ const SongReady = () => {
<FlatList
style={{ flex: 1 }}
data={musicUrls}
keyExtractor={(item, idx) => `${item || "song"}-${idx}`}
keyExtractor={(item, idx) => `${item || 'song'}-${idx}`}
renderItem={({ item, index }) => (
<SongOptionCard
index={index}
@@ -301,8 +299,8 @@ const SongReady = () => {
<View
style={{
paddingBottom: gutters * 2,
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
gap: 12,
}}
>
@@ -311,10 +309,7 @@ const SongReady = () => {
onPress={handleChooseTrack}
disabled={!musicUrls?.length}
/>
<BorderGradientButton
title="Re-générer le morceau"
onPress={handleRegeneratePress}
/>
<BorderGradientButton title="Re-générer le morceau" onPress={handleRegeneratePress} />
</View>
{!isWeb && (
<>
@@ -331,8 +326,8 @@ const SongReady = () => {
</>
)}
</Page>
);
};
)
}
const SongOptionCard = ({
index,
@@ -345,11 +340,9 @@ const SongOptionCard = ({
selectedProject,
}) => {
const trackTitle =
(Array.isArray(selectedProject?.musicTitles)
? selectedProject.musicTitles?.[index]
: null) ||
(Array.isArray(selectedProject?.musicTitles) ? selectedProject.musicTitles?.[index] : null) ||
selectedProject?.title ||
`Morceau ${index + 1}`;
`Morceau ${index + 1}`
const player = useSharedAudioPlayer(url ? { uri: url } : undefined, {
id: url ? `songready-${url}` : undefined,
@@ -357,130 +350,124 @@ const SongOptionCard = ({
artwork: selectedProject?.coverUrl || null,
coverUrl: selectedProject?.coverUrl || null,
metadata: { index, projectId },
});
})
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const [isPlaying, setIsPlaying] = useState(false);
const shouldResumeAfterSeekRef = useRef(false);
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
const [isPlaying, setIsPlaying] = useState(false)
const shouldResumeAfterSeekRef = useRef(false)
useEffect(() => {
registerPlayer(index, player);
return () => registerPlayer(index, null);
}, [index, player, registerPlayer]);
registerPlayer(index, player)
return () => registerPlayer(index, null)
}, [index, player, registerPlayer])
useEffect(() => {
if (!player || !url) return;
if (!player || !url) return
const preload = async () => {
try {
await player.load?.({ startPositionMs: 0 });
await player.load?.({ startPositionMs: 0 })
} catch (error) {
console.log("SongReady preload error", error?.message);
console.log('SongReady preload error', error?.message)
}
};
preload();
}, [player, url]);
}
preload()
}, [player, url])
useEffect(() => {
if (!player) {
setIsPlaying(false);
setProgressInfo({ pos: 0, dur: 0 });
return undefined;
setIsPlaying(false)
setProgressInfo({ pos: 0, dur: 0 })
return undefined
}
const id = setInterval(() => {
const durationMs = Math.max(
0,
Math.round((Number(player.duration) || 0) * 1000),
);
const positionMs = Math.max(
0,
Math.round((Number(player.currentTime) || 0) * 1000),
);
const durationMs = Math.max(0, Math.round((Number(player.duration) || 0) * 1000))
const positionMs = Math.max(0, Math.round((Number(player.currentTime) || 0) * 1000))
setProgressInfo((prev) => {
if (
Math.abs((prev?.dur || 0) - durationMs) < 5 &&
Math.abs((prev?.pos || 0) - positionMs) < 5
) {
return prev;
return prev
}
return { pos: positionMs, dur: durationMs };
});
return { pos: positionMs, dur: durationMs }
})
setIsPlaying((prev) => {
const next = !!player.playing;
const next = !!player.playing
if (prev !== next) {
return next;
return next
}
return prev;
});
}, 300);
return () => clearInterval(id);
}, [player]);
return prev
})
}, 300)
return () => clearInterval(id)
}, [player])
const hasCapturedSeekStateRef = useRef(false);
const hasCapturedSeekStateRef = useRef(false)
const resumePlaybackIfNeeded = useCallback(async () => {
if (!player) return;
const shouldResume = shouldResumeAfterSeekRef.current;
shouldResumeAfterSeekRef.current = false;
hasCapturedSeekStateRef.current = false; // Reset capture flag
if (!player) return
const shouldResume = shouldResumeAfterSeekRef.current
shouldResumeAfterSeekRef.current = false
hasCapturedSeekStateRef.current = false // Reset capture flag
if (!shouldResume) return;
if (!shouldResume) return
try {
if (player.resume) {
await player.resume?.();
await player.resume?.()
} else {
await player.play?.();
await player.play?.()
}
} catch (error) {
console.log("SongReady resume after seek", error?.message);
console.log('SongReady resume after seek', error?.message)
}
}, [player]);
}, [player])
const handleSeek = useCallback(
async (targetMs) => {
if (!player || !progressInfo?.dur) {
return;
return
}
const dur = progressInfo.dur || 0;
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)));
const dur = progressInfo.dur || 0
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)))
// Mise à jour visuelle immédiate
setProgressInfo((prev) => ({ ...prev, pos }));
setProgressInfo((prev) => ({ ...prev, pos }))
try {
await player.seekTo?.(Math.floor(pos / 1000));
await player.seekTo?.(Math.floor(pos / 1000))
} catch (error) {
console.log("SongReady seek error", error?.message);
console.log('SongReady seek error', error?.message)
}
},
[player, progressInfo?.dur],
);
[player, progressInfo?.dur]
)
const handleSeekStart = useCallback(async () => {
onSelect(index);
if (!player) return;
onSelect(index)
if (!player) return
const isCurrentlyPlaying = !!player.playing || isPlaying;
const isCurrentlyPlaying = !!player.playing || isPlaying
// Only capture state if we haven't already for this drag interaction
if (!hasCapturedSeekStateRef.current) {
hasCapturedSeekStateRef.current = true;
shouldResumeAfterSeekRef.current = isCurrentlyPlaying;
hasCapturedSeekStateRef.current = true
shouldResumeAfterSeekRef.current = isCurrentlyPlaying
}
try {
if (isCurrentlyPlaying) {
await player.pause?.();
await player.pause?.()
}
} catch (error) {
console.log("SongReady pause on seek start", error?.message);
console.log('SongReady pause on seek start', error?.message)
}
}, [index, isPlaying, onSelect, player]);
}, [index, isPlaying, onSelect, player])
const handleToggle = () => {
onSelect(index);
onTogglePlayback?.(index, isPlaying);
};
onSelect(index)
onTogglePlayback?.(index, isPlaying)
}
return (
<Pressable
@@ -490,7 +477,7 @@ const SongOptionCard = ({
borderRadius: 20,
borderWidth: isSelected ? 2 : 1,
borderColor: isSelected ? Palette.primary : Palette.ultraLightWhite,
overflow: "hidden",
overflow: 'hidden',
},
pressed && { opacity: 0.96 },
]}
@@ -500,15 +487,15 @@ const SongOptionCard = ({
tint="dark"
style={{
borderRadius: 18,
overflow: "hidden",
overflow: 'hidden',
padding: 12,
backgroundColor: "#FFFFFF0A",
backgroundColor: '#FFFFFF0A',
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 12,
}}
>
@@ -524,7 +511,7 @@ const SongOptionCard = ({
<View style={{ flex: 1 }}>
<Text
style={{
color: "white",
color: 'white',
marginBottom: responsiveHeight(1),
}}
>{`Morceau ${index + 1}`}</Text>
@@ -551,9 +538,9 @@ const SongOptionCard = ({
height: 18,
borderRadius: 9,
borderWidth: 2,
borderColor: isSelected ? Palette.primary : "white",
alignItems: "center",
justifyContent: "center",
borderColor: isSelected ? Palette.primary : 'white',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isSelected && (
@@ -571,44 +558,34 @@ const SongOptionCard = ({
</View>
</BlurView>
</Pressable>
);
};
)
}
const RegenerateModal = ({ visible, onClose, onConfirm }) => {
const { width, height } = useWindowDimensions();
const isCompactWidth = width < 420;
const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12);
const verticalPadding = Math.max(
isCompactWidth ? gutters : gutters * 1.5,
12,
);
const availableWidth = Math.max(width - horizontalPadding * 2, 0);
const { width, height } = useWindowDimensions()
const isCompactWidth = width < 420
const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12)
const verticalPadding = Math.max(isCompactWidth ? gutters : gutters * 1.5, 12)
const availableWidth = Math.max(width - horizontalPadding * 2, 0)
const contentWidth =
availableWidth > 0
? Math.min(REGENERATE_MODAL_MAX_WIDTH, availableWidth)
: undefined;
const contentGap = isCompactWidth ? 18 : 24;
const buttonGap = isCompactWidth ? 12 : 15;
availableWidth > 0 ? Math.min(REGENERATE_MODAL_MAX_WIDTH, availableWidth) : undefined
const contentGap = isCompactWidth ? 18 : 24
const buttonGap = isCompactWidth ? 12 : 15
return (
<Modal
animationType="fade"
transparent
visible={visible}
onRequestClose={onClose}
>
<Modal animationType="fade" transparent visible={visible} onRequestClose={onClose}>
<View
style={{
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.6)",
backgroundColor: 'rgba(0, 0, 0, 0.6)',
}}
>
<ScrollView
bounces={false}
contentContainerStyle={{
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: horizontalPadding,
paddingVertical: verticalPadding,
minHeight: height,
@@ -618,20 +595,20 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
>
<CreateLyricsHeader
containerStyle={{
width: contentWidth ?? "100%",
width: contentWidth ?? '100%',
maxWidth: REGENERATE_MODAL_MAX_WIDTH,
alignSelf: "center",
alignSelf: 'center',
paddingVertical: contentGap,
paddingHorizontal: contentGap,
gap: contentGap,
flexShrink: 1,
}}
>
<View style={{ gap: contentGap, alignItems: "center" }}>
<View style={{ gap: contentGap, alignItems: 'center' }}>
<View
style={{
gap: isCompactWidth ? 10 : 12,
alignItems: "center",
alignItems: 'center',
paddingHorizontal: isCompactWidth ? 4 : 12,
}}
>
@@ -640,7 +617,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Re-générer le morceau ?
@@ -650,17 +627,17 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
{`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`}
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -669,7 +646,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Cette action coûte
@@ -681,7 +658,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
/>
</View>
@@ -690,7 +667,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Les crédits seront utilisés lors de l'étape de génération.
@@ -698,15 +675,12 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
</View>
<View
style={{
width: isCompactWidth ? "100%" : "85%",
alignSelf: "center",
width: isCompactWidth ? '100%' : '85%',
alignSelf: 'center',
gap: buttonGap,
}}
>
<GradientButton
title="Oui, modifier mes choix"
onPress={onConfirm}
/>
<GradientButton title="Oui, modifier mes choix" onPress={onConfirm} />
<BorderGradientButton title="Retour" onPress={onClose} />
</View>
</View>
@@ -714,7 +688,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
</ScrollView>
</View>
</Modal>
);
};
)
}
export default SongReady;
export default SongReady
+50 -62
View File
@@ -1,25 +1,25 @@
import React, { useMemo, useState } from "react";
import { Pressable, ScrollView, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import alert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { isWeb } from "../../hooks/useLayoutType";
import React, { useMemo, useState } from 'react'
import { Pressable, ScrollView, Text, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import alert from '../../components/Alert'
import GradientButton from '../../components/GradientButton'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters } from '../../styles'
import { isWeb } from '../../hooks/useLayoutType'
const Studio = () => {
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [selectedProjectId, setSelectedProjectId] = useState(null)
const { userProjects: projects, selectProject } = useUser();
const { userProjects: projects, selectProject } = useUser()
const selectedProject = useMemo(() => {
if (!Array.isArray(projects) || !projects.length) return null;
return projects.find((p) => p?.id === selectedProjectId) ?? null;
}, [projects, selectedProjectId]);
if (!Array.isArray(projects) || !projects.length) return null
return projects.find((p) => p?.id === selectedProjectId) ?? null
}, [projects, selectedProjectId])
return (
<Page
@@ -35,95 +35,83 @@ const Studio = () => {
<View style={{ marginBottom: gutters * 2 }}>
<Text
style={{
color: "#fff",
color: '#fff',
fontSize: 18,
marginBottom: 12,
fontWeight: "600",
fontWeight: '600',
}}
>
Derniers projets générés
</Text>
<ScrollView contentContainerStyle={{ gap: 10, paddingRight: 6 }}>
{projects.map((p) => {
const couplet = p?.lyrics?.couplet || "";
const preview = couplet.split("\n").slice(0, 2).join(" ");
const isSelected = selectedProjectId === p?.id;
const couplet = p?.lyrics?.couplet || ''
const preview = couplet.split('\n').slice(0, 2).join(' ')
const isSelected = selectedProjectId === p?.id
const createdAt = p?.createdAt?.toDate
? p.createdAt.toDate()
: p?.createdAt
? new Date(p.createdAt)
: null;
: null
const createdLabel =
createdAt && !Number.isNaN(createdAt.getTime())
? `${createdAt.toLocaleDateString("fr-FR")} • ${createdAt.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}`
: "";
? `${createdAt.toLocaleDateString('fr-FR')} • ${createdAt.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`
: ''
return (
<Pressable
key={p.id}
onPress={() => setSelectedProjectId(p?.id ?? null)}
style={{
backgroundColor: isSelected
? "rgba(15, 12, 25, 0.75)"
: "rgba(15, 12, 25, 0.2)",
? 'rgba(15, 12, 25, 0.75)'
: 'rgba(15, 12, 25, 0.2)',
borderRadius: 12,
padding: 12,
borderWidth: isSelected ? 2 : 0,
borderColor: isSelected ? "#F94697" : "transparent",
shadowColor: "rgba(0, 0, 0, 0.55)",
borderColor: isSelected ? '#F94697' : 'transparent',
shadowColor: 'rgba(0, 0, 0, 0.55)',
shadowOpacity: isSelected ? 0.6 : 0.35,
shadowRadius: isSelected ? 8 : 4,
shadowOffset: { width: 0, height: 4 },
elevation: isSelected ? 6 : 1,
}}
>
<Text
style={{ color: "#fff", fontSize: 16, fontWeight: "600" }}
>
{p?.title || "Sans titre"}
<Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>
{p?.title || 'Sans titre'}
</Text>
{!!createdLabel && (
<Text
style={{ color: "#bbb", marginTop: 4, fontSize: 12 }}
>
<Text style={{ color: '#bbb', marginTop: 4, fontSize: 12 }}>
{createdLabel}
</Text>
)}
{!!preview && (
<Text
style={{ color: "#ddd", marginTop: 6 }}
numberOfLines={2}
>
<Text style={{ color: '#ddd', marginTop: 6 }} numberOfLines={2}>
{preview}
</Text>
)}
{p?.config?.style && (
<Text
style={{ color: "#aaa", marginTop: 6, fontSize: 12 }}
>
<Text style={{ color: '#aaa', marginTop: 6, fontSize: 12 }}>
Style: {String(p.config.style)}
</Text>
)}
</Pressable>
);
)
})}
</ScrollView>
</View>
)}
</View>
<View style={{ justifyContent: "flex-end" }}>
<View style={{ justifyContent: 'flex-end' }}>
<GradientButton
title="Commencer"
disabled={!selectedProject}
onPress={() => {
if (selectedProject?.musicStatus === "GENERATING") {
alert(
"Attention",
"La chanson est en cours de génération. Veuillez patienter.",
);
if (selectedProject?.musicStatus === 'GENERATING') {
alert('Attention', 'La chanson est en cours de génération. Veuillez patienter.')
} else {
if (!selectedProject?.id) return;
selectProject(selectedProject.id);
navigate(Routes.Compose);
if (!selectedProject?.id) return
selectProject(selectedProject.id)
navigate(Routes.Compose)
}
}}
/>
@@ -135,9 +123,9 @@ const Studio = () => {
marginTop: responsiveHeight(2),
}}
onPress={() => {
if (!selectedProject?.id) return;
selectProject(selectedProject.id);
navigate(Routes.SongReady);
if (!selectedProject?.id) return
selectProject(selectedProject.id)
navigate(Routes.SongReady)
}}
/>
{!selectedProject?.coverUrl && (
@@ -147,9 +135,9 @@ const Studio = () => {
marginTop: responsiveHeight(2),
}}
onPress={() => {
if (!selectedProject?.id) return;
selectProject(selectedProject.id);
navigate(Routes.PouchReady);
if (!selectedProject?.id) return
selectProject(selectedProject.id)
navigate(Routes.PouchReady)
}}
/>
)}
@@ -157,7 +145,7 @@ const Studio = () => {
)}
</View>
</Page>
);
};
)
}
export default Studio;
export default Studio
File diff suppressed because it is too large Load Diff
+9 -13
View File
@@ -1,9 +1,9 @@
import React from "reactn";
import { Text } from "react-native";
import React from 'reactn'
import { Text } from 'react-native'
import Page from "../layouts/Page";
import Page from '../layouts/Page'
import { Fonts } from "../styles";
import { Fonts } from '../styles'
const terms = `Conditions Générales d'Utilisation (CGU) de minuit.starter
@@ -62,16 +62,12 @@ Si une disposition des présentes CGU est jugée inapplicable, les autres rester
Contact
Pour toute question : hello@minuit.agency.
Dernière mise à jour : 22/10/2023.`;
Dernière mise à jour : 22/10/2023.`
export default ({ navigation, route }) => {
return (
<Page
headerType="NAVIGATE"
title={"Conditions Générales d'Utilisation"}
scrollEnabled
>
<Text style={{ ...Fonts({ type: "default" }) }}>{terms}</Text>
<Page headerType="NAVIGATE" title={"Conditions Générales d'Utilisation"} scrollEnabled>
<Text style={{ ...Fonts({ type: 'default' }) }}>{terms}</Text>
</Page>
);
};
)
}
+174 -199
View File
@@ -1,230 +1,212 @@
import React, { useMemo, useRef, useState } from "react";
import { Dimensions, KeyboardAvoidingView, Platform, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { sanitizeStructureList } from "../../utils/songStructure";
import CreatingLyrics from "./CreatingLyrics";
import CustomizeSongStructure from "./CustomizeSongStructure";
import EmotionConvey from "./EmotionConvey";
import Goals from "./Goals";
import Rhymes from "./Rhymes";
import SongStructure from "./SongStructure";
import SongStyle from "./SongStyle";
import SongTo from "./SongTo";
import SpecificityContext from "./SpecificityContext";
const { width: windowWidth } = Dimensions.get("window");
const MAX_STEP_INDEX = 8;
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false };
import React, { useMemo, useRef, useState } from 'react'
import { Dimensions, KeyboardAvoidingView, Platform, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { SwiperFlatList } from 'react-native-swiper-flatlist'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from '../../data/data'
import Page from '../../layouts/Page'
import { goBack } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters } from '../../styles'
import { sanitizeStructureList } from '../../utils/songStructure'
import CreatingLyrics from './CreatingLyrics'
import CustomizeSongStructure from './CustomizeSongStructure'
import EmotionConvey from './EmotionConvey'
import Goals from './Goals'
import Rhymes from './Rhymes'
import SongStructure from './SongStructure'
import SongStyle from './SongStyle'
import SongTo from './SongTo'
import SpecificityContext from './SpecificityContext'
const { width: windowWidth } = Dimensions.get('window')
const MAX_STEP_INDEX = 8
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false }
const CreateLyricsWithAi = () => {
const { selectedProject } = useUser();
const scrollRef = useRef(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
const containerWidth = windowWidth;
const { selectedProject } = useUser()
const scrollRef = useRef(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(16)
const [parentLayout, setparentLayout] = useState(null)
const containerWidth = windowWidth
const handleLyricsError = React.useCallback(() => {
setSelectedIndex(7);
}, [setSelectedIndex]);
setSelectedIndex(7)
}, [setSelectedIndex])
// Collected state across steps
const [objective, setObjective] = useState(null); // from Goals list
const [otherObjective, setOtherObjective] = useState("");
const [context, setContext] = useState("");
const [emotion, setEmotion] = useState(null);
const [style, setStyle] = useState(null); // from list
const [otherStyle, setOtherStyle] = useState("");
const [audience, setAudience] = useState("");
const [structure, setStructure] = useState(null); // selected structure string
const [rhymes, setRhymes] = useState(null);
const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain']
const [objective, setObjective] = useState(null) // from Goals list
const [otherObjective, setOtherObjective] = useState('')
const [context, setContext] = useState('')
const [emotion, setEmotion] = useState(null)
const [style, setStyle] = useState(null) // from list
const [otherStyle, setOtherStyle] = useState('')
const [audience, setAudience] = useState('')
const [structure, setStructure] = useState(null) // selected structure string
const [rhymes, setRhymes] = useState(null)
const [customStructure, setCustomStructure] = useState(null) // array like ['couplet','refrain']
const trimmedOtherObjective =
typeof otherObjective === "string" ? otherObjective.trim() : "";
const shouldUseOtherObjective = objective === OTHER_OBJECTIVE_OPTION;
const persistedOtherObjective = shouldUseOtherObjective
? trimmedOtherObjective
: "";
const trimmedOtherObjective = typeof otherObjective === 'string' ? otherObjective.trim() : ''
const shouldUseOtherObjective = objective === OTHER_OBJECTIVE_OPTION
const persistedOtherObjective = shouldUseOtherObjective ? trimmedOtherObjective : ''
const trimmedOtherStyle =
typeof otherStyle === "string" ? otherStyle.trim() : "";
const shouldUseOtherStyle = style === OTHER_STYLE_OPTION;
const persistedOtherStyle = shouldUseOtherStyle ? trimmedOtherStyle : "";
const trimmedOtherStyle = typeof otherStyle === 'string' ? otherStyle.trim() : ''
const shouldUseOtherStyle = style === OTHER_STYLE_OPTION
const persistedOtherStyle = shouldUseOtherStyle ? trimmedOtherStyle : ''
// Pré-remplir les états depuis le projet sélectionné si disponibles
React.useEffect(() => {
if (!selectedProject) return;
const sel = selectedProject?.selections || {};
const cfg = selectedProject?.config || {};
if (!selectedProject) return
const sel = selectedProject?.selections || {}
const cfg = selectedProject?.config || {}
const rawOtherObjective =
typeof sel.otherObjective === "string" ? sel.otherObjective : "";
const trimmedSavedOtherObjective = rawOtherObjective.trim();
const hasSavedOtherObjective = trimmedSavedOtherObjective.length > 0;
const rawOtherObjective = typeof sel.otherObjective === 'string' ? sel.otherObjective : ''
const trimmedSavedOtherObjective = rawOtherObjective.trim()
const hasSavedOtherObjective = trimmedSavedOtherObjective.length > 0
const savedObjective =
typeof sel.objective === "string" && sel.objective.trim().length > 0
typeof sel.objective === 'string' && sel.objective.trim().length > 0
? sel.objective.trim()
: null;
: null
if (!otherObjective && hasSavedOtherObjective) {
setOtherObjective(rawOtherObjective);
setOtherObjective(rawOtherObjective)
}
const shouldSelectOtherObjective =
hasSavedOtherObjective &&
(!savedObjective || savedObjective === OTHER_OBJECTIVE_OPTION);
hasSavedOtherObjective && (!savedObjective || savedObjective === OTHER_OBJECTIVE_OPTION)
const resolvedSavedObjective = shouldSelectOtherObjective
? OTHER_OBJECTIVE_OPTION
: savedObjective;
: savedObjective
if (resolvedSavedObjective && objective !== resolvedSavedObjective) {
setObjective(resolvedSavedObjective);
setObjective(resolvedSavedObjective)
} else if (!resolvedSavedObjective && objective !== null) {
setObjective(null);
setObjective(null)
}
if (!context && typeof sel.context === "string") setContext(sel.context);
if (!context && typeof sel.context === 'string') setContext(sel.context)
// Emotion: accepter objet {title, description} ou string "Titre : description"
if (emotion == null && sel.emotion) {
if (typeof sel.emotion === "object" && sel.emotion.title) {
if (typeof sel.emotion === 'object' && sel.emotion.title) {
setEmotion({
title: sel.emotion.title,
description: sel.emotion.description || "",
});
} else if (typeof sel.emotion === "string") {
const [t, d] = sel.emotion.split(":");
const title = (t || "").trim();
const description = (d || "").trim();
if (title) setEmotion({ title, description });
description: sel.emotion.description || '',
})
} else if (typeof sel.emotion === 'string') {
const [t, d] = sel.emotion.split(':')
const title = (t || '').trim()
const description = (d || '').trim()
if (title) setEmotion({ title, description })
}
}
const rawOtherStyle =
typeof sel.otherStyle === "string" ? sel.otherStyle : "";
const trimmedSavedOtherStyle = rawOtherStyle.trim();
const hasSavedOtherStyle = trimmedSavedOtherStyle.length > 0;
const rawOtherStyle = typeof sel.otherStyle === 'string' ? sel.otherStyle : ''
const trimmedSavedOtherStyle = rawOtherStyle.trim()
const hasSavedOtherStyle = trimmedSavedOtherStyle.length > 0
const savedStyle =
typeof sel.style === "string" && sel.style.trim().length > 0
? sel.style.trim()
: null;
typeof sel.style === 'string' && sel.style.trim().length > 0 ? sel.style.trim() : null
if (!otherStyle && hasSavedOtherStyle) {
setOtherStyle(rawOtherStyle);
setOtherStyle(rawOtherStyle)
}
const shouldSelectOtherStyle =
hasSavedOtherStyle && (!savedStyle || savedStyle === OTHER_STYLE_OPTION);
const resolvedSavedStyle = shouldSelectOtherStyle
? OTHER_STYLE_OPTION
: savedStyle;
hasSavedOtherStyle && (!savedStyle || savedStyle === OTHER_STYLE_OPTION)
const resolvedSavedStyle = shouldSelectOtherStyle ? OTHER_STYLE_OPTION : savedStyle
if (resolvedSavedStyle && style !== resolvedSavedStyle) {
setStyle(resolvedSavedStyle);
setStyle(resolvedSavedStyle)
} else if (!resolvedSavedStyle && style !== null) {
setStyle(null);
setStyle(null)
}
if (!audience && typeof sel.audience === "string")
setAudience(sel.audience);
if (!audience && typeof sel.audience === 'string') setAudience(sel.audience)
// Structure choisie (string) si déjà enregistrée dans selections
if (structure == null && typeof sel.structure === "string" && sel.structure)
setStructure(sel.structure);
if (structure == null && typeof sel.structure === 'string' && sel.structure)
setStructure(sel.structure)
// Rimes
if (rhymes == null && typeof sel.rhymes === "string" && sel.rhymes)
setRhymes(sel.rhymes);
if (rhymes == null && typeof sel.rhymes === 'string' && sel.rhymes) setRhymes(sel.rhymes)
// Structure personnalisée: prioriser selections.customStructure puis config.structure
const savedCustom = Array.isArray(sel.customStructure)
? sanitizeStructureList(sel.customStructure, CUSTOM_SANITIZE_OPTIONS)
: null;
: null
const cfgStructure = Array.isArray(cfg.structure)
? sanitizeStructureList(cfg.structure, CUSTOM_SANITIZE_OPTIONS)
: null;
const fallbackStructure =
savedCustom && savedCustom.length ? savedCustom : cfgStructure;
: null
const fallbackStructure = savedCustom && savedCustom.length ? savedCustom : cfgStructure
if (!Array.isArray(customStructure) && fallbackStructure?.length) {
setCustomStructure(fallbackStructure);
setCustomStructure(fallbackStructure)
} else if (
Array.isArray(customStructure) &&
fallbackStructure?.length &&
(customStructure.length !== fallbackStructure.length ||
customStructure.some((value, idx) => value !== fallbackStructure[idx]))
) {
setCustomStructure(fallbackStructure);
setCustomStructure(fallbackStructure)
}
}, [selectedProject]);
}, [selectedProject])
const parsedStructure = useMemo(() => {
// Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain"
try {
if (!structure || typeof structure !== "string") return null;
const parts = structure.split(",");
const result = [];
if (!structure || typeof structure !== 'string') return null
const parts = structure.split(',')
const result = []
parts.forEach((seg) => {
const s = seg.trim().toLowerCase();
const coupletMatch = s.match(/(\d+)\s+couplet/);
const refrainMatch = s.match(/(\d+)\s+refrain/);
const s = seg.trim().toLowerCase()
const coupletMatch = s.match(/(\d+)\s+couplet/)
const refrainMatch = s.match(/(\d+)\s+refrain/)
if (coupletMatch) {
const count = parseInt(coupletMatch[1], 10);
for (let i = 0; i < count; i++) result.push("couplet");
const count = parseInt(coupletMatch[1], 10)
for (let i = 0; i < count; i++) result.push('couplet')
}
if (refrainMatch) {
const count = parseInt(refrainMatch[1], 10);
for (let i = 0; i < count; i++) result.push("refrain");
const count = parseInt(refrainMatch[1], 10)
for (let i = 0; i < count; i++) result.push('refrain')
}
});
const sanitizedResult = sanitizeStructureList(result);
return sanitizedResult.length ? sanitizedResult : null;
})
const sanitizedResult = sanitizeStructureList(result)
return sanitizedResult.length ? sanitizedResult : null
} catch (e) {
return null;
return null
}
}, [structure]);
}, [structure])
const sanitizedCustomStructure = useMemo(() => {
if (!Array.isArray(customStructure) || customStructure.length === 0) {
return [];
return []
}
return sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS);
}, [customStructure]);
return sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS)
}, [customStructure])
const sanitizedParsedStructure = useMemo(() => {
if (!Array.isArray(parsedStructure) || parsedStructure.length === 0) {
return [];
return []
}
return sanitizeStructureList(parsedStructure, CUSTOM_SANITIZE_OPTIONS);
}, [parsedStructure]);
return sanitizeStructureList(parsedStructure, CUSTOM_SANITIZE_OPTIONS)
}, [parsedStructure])
const fallbackProjectStructure = useMemo(() => {
const raw = selectedProject?.config?.structure;
if (!Array.isArray(raw) || raw.length === 0) return [];
return sanitizeStructureList(raw, CUSTOM_SANITIZE_OPTIONS);
}, [selectedProject]);
const raw = selectedProject?.config?.structure
if (!Array.isArray(raw) || raw.length === 0) return []
return sanitizeStructureList(raw, CUSTOM_SANITIZE_OPTIONS)
}, [selectedProject])
const baseStructureForCustomizer = useMemo(() => {
if (sanitizedCustomStructure.length > 0) return sanitizedCustomStructure;
if (sanitizedParsedStructure.length > 0) return sanitizedParsedStructure;
return fallbackProjectStructure;
}, [
sanitizedCustomStructure,
sanitizedParsedStructure,
fallbackProjectStructure,
]);
if (sanitizedCustomStructure.length > 0) return sanitizedCustomStructure
if (sanitizedParsedStructure.length > 0) return sanitizedParsedStructure
return fallbackProjectStructure
}, [sanitizedCustomStructure, sanitizedParsedStructure, fallbackProjectStructure])
const lyricsConfig = useMemo(() => {
const resolvedObjective = shouldUseOtherObjective
? persistedOtherObjective || undefined
: objective || undefined;
: objective || undefined
const resolvedStyle = shouldUseOtherStyle
? persistedOtherStyle || undefined
: style || undefined;
const sanitizedCustom = sanitizedCustomStructure;
const sanitizedParsed = sanitizedParsedStructure;
: style || undefined
const sanitizedCustom = sanitizedCustomStructure
const sanitizedParsed = sanitizedParsedStructure
return {
objective: resolvedObjective,
@@ -242,7 +224,7 @@ const CreateLyricsWithAi = () => {
? sanitizedParsed
: undefined,
rhymes: rhymes || undefined,
};
}
}, [
shouldUseOtherObjective,
persistedOtherObjective,
@@ -256,48 +238,44 @@ const CreateLyricsWithAi = () => {
sanitizedParsedStructure,
rhymes,
sanitizedCustomStructure,
]);
])
const isNextDisabled = React.useMemo(() => {
switch (selectedIndex) {
case 0: {
const hasObjective =
typeof objective === "string" && objective.length > 0;
const hasOther = trimmedOtherObjective.length > 0;
return !(hasObjective || hasOther);
const hasObjective = typeof objective === 'string' && objective.length > 0
const hasOther = trimmedOtherObjective.length > 0
return !(hasObjective || hasOther)
}
case 1: {
const hasContext =
typeof context === "string" && context.trim().length > 0;
return !hasContext;
const hasContext = typeof context === 'string' && context.trim().length > 0
return !hasContext
}
case 2: {
const hasEmotion =
(emotion && typeof emotion === "object" && !!emotion.title) ||
(typeof emotion === "string" && emotion.trim().length > 0);
return !hasEmotion;
(emotion && typeof emotion === 'object' && !!emotion.title) ||
(typeof emotion === 'string' && emotion.trim().length > 0)
return !hasEmotion
}
case 3: {
const hasStyle = typeof style === "string" && style.length > 0;
const hasOther = trimmedOtherStyle.length > 0;
return !(hasStyle || hasOther);
const hasStyle = typeof style === 'string' && style.length > 0
const hasOther = trimmedOtherStyle.length > 0
return !(hasStyle || hasOther)
}
case 4: {
const hasAudience =
typeof audience === "string" && audience.trim().length > 0;
return !hasAudience;
const hasAudience = typeof audience === 'string' && audience.trim().length > 0
return !hasAudience
}
case 5: {
const hasStructure =
typeof structure === "string" && structure.length > 0;
return !hasStructure;
const hasStructure = typeof structure === 'string' && structure.length > 0
return !hasStructure
}
case 7: {
const hasRhymes = typeof rhymes === "string" && rhymes.length > 0;
return !hasRhymes;
const hasRhymes = typeof rhymes === 'string' && rhymes.length > 0
return !hasRhymes
}
default:
return false;
return false
}
}, [
selectedIndex,
@@ -310,74 +288,71 @@ const CreateLyricsWithAi = () => {
audience,
structure,
rhymes,
]);
])
const onPressNext = async () => {
setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX));
};
setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX))
}
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex((idx) => Math.max(0, idx - 1));
setSelectedIndex((idx) => Math.max(0, idx - 1))
} else {
goBack();
goBack()
}
};
}
// Sync scroll position and progress with selectedIndex
React.useEffect(() => {
if (!parentLayout?.height) return;
const nextProgress = Math.max(0, Math.min(100, 16 + selectedIndex * 9));
setProgress(nextProgress);
if (!parentLayout?.height) return
const nextProgress = Math.max(0, Math.min(100, 16 + selectedIndex * 9))
setProgress(nextProgress)
const ref = scrollRef.current;
if (!ref || typeof ref.scrollToIndex !== "function") {
return;
const ref = scrollRef.current
if (!ref || typeof ref.scrollToIndex !== 'function') {
return
}
let cancelled = false;
let cancelled = false
const attemptScroll = (retries = 0) => {
if (cancelled) return;
if (cancelled) return
try {
ref.scrollToIndex({ index: selectedIndex, animated: true });
ref.scrollToIndex({ index: selectedIndex, animated: true })
} catch (error) {
if (retries < 3) {
setTimeout(() => attemptScroll(retries + 1), 40);
setTimeout(() => attemptScroll(retries + 1), 40)
} else {
console.warn("[CreateLyricsWithAi] scrollToIndex failed", {
console.warn('[CreateLyricsWithAi] scrollToIndex failed', {
index: selectedIndex,
error,
});
})
}
}
};
}
attemptScroll();
attemptScroll()
return () => {
cancelled = true;
};
}, [selectedIndex, parentLayout?.height]);
cancelled = true
}
}, [selectedIndex, parentLayout?.height])
return (
<Page>
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{
flex: 1,
paddingBottom: gutters,
gap: responsiveHeight(5),
}}
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 100}
>
<View
style={{ flex: 1 }}
onLayout={(e) => setparentLayout(e.nativeEvent.layout)}
>
<View style={{ flex: 1 }} onLayout={(e) => setparentLayout(e.nativeEvent.layout)}>
<SwiperFlatList
style={
Platform.OS === "web"
Platform.OS === 'web'
? { width: containerWidth }
: { width: containerWidth, left: -gutters }
}
@@ -501,15 +476,15 @@ const CreateLyricsWithAi = () => {
{selectedIndex !== 8 && (
<GradientButton
maxWidth={500}
title={selectedIndex === 7 ? "Générer" : "Suivant"}
title={selectedIndex === 7 ? 'Générer' : 'Suivant'}
disabled={isNextDisabled}
onPress={onPressNext}
containerStyle={{ alignSelf: "center", width: "100%" }}
containerStyle={{ alignSelf: 'center', width: '100%' }}
/>
)}
</KeyboardAvoidingView>
</Page>
);
};
)
}
export default CreateLyricsWithAi;
export default CreateLyricsWithAi

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