feat: fixes and formatter
This commit is contained in:
+114
-121
@@ -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
@@ -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
@@ -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'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'en bas pour afficher les actions
|
||||
@@ -542,7 +514,7 @@ const CreatePassword = () => {
|
||||
</View>
|
||||
</Modal>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatePassword;
|
||||
export default CreatePassword
|
||||
|
||||
+42
-45
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+367
-457
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
)
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+323
-427
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user