feat: fixes and formatter
This commit is contained in:
@@ -1,171 +1,160 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Modal, Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { Input } from "../../components/Input";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import firebase, { projectsRef, usersRef } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUserData } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { buildFullName } from "../../utils/artistName";
|
||||
import { background } from "../../assets";
|
||||
import { BlurView } from 'expo-blur'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import { Input } from '../../components/Input'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import firebase, { projectsRef, usersRef } from '../../config/firebase'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import { buildFullName } from '../../utils/artistName'
|
||||
import { background } from '../../assets'
|
||||
|
||||
export const ChooseCoverType = () => {
|
||||
const [showIntro, setShowIntro] = useState(true);
|
||||
const [choiceVisible, setChoiceVisible] = useState(false);
|
||||
const [pseudoVisible, setPseudoVisible] = useState(false);
|
||||
const [pseudo, setPseudo] = useState("");
|
||||
const [pendingAction, setPendingAction] = useState(null);
|
||||
const [savingChoice, setSavingChoice] = useState(false);
|
||||
const [savingPseudo, setSavingPseudo] = useState(false);
|
||||
const [showIntro, setShowIntro] = useState(true)
|
||||
const [choiceVisible, setChoiceVisible] = useState(false)
|
||||
const [pseudoVisible, setPseudoVisible] = useState(false)
|
||||
const [pseudo, setPseudo] = useState('')
|
||||
const [pendingAction, setPendingAction] = useState(null)
|
||||
const [savingChoice, setSavingChoice] = useState(false)
|
||||
const [savingPseudo, setSavingPseudo] = useState(false)
|
||||
|
||||
const { selectedProject, selectedProjectId, currentUserData, currentUID } =
|
||||
useUserData();
|
||||
const { setTooltip } = useMinuit();
|
||||
const { selectedProject, selectedProjectId, currentUserData, currentUID } = useUserData()
|
||||
const { setTooltip } = useMinuit()
|
||||
|
||||
const projectId = selectedProject?.id || selectedProjectId || null;
|
||||
const hasFinalCover = !!selectedProject?.coverUrl;
|
||||
const projectId = selectedProject?.id || selectedProjectId || null
|
||||
const hasFinalCover = !!selectedProject?.coverUrl
|
||||
const hasGeneratedOptions = Array.isArray(selectedProject?.cover?.options)
|
||||
? selectedProject.cover.options.length > 0
|
||||
: false;
|
||||
: false
|
||||
|
||||
const hasArtistPreference = useMemo(() => {
|
||||
if (currentUserData?.userName) return true;
|
||||
const pref = currentUserData?.artistNamePreference;
|
||||
return pref === "REAL_NAME" || pref === "CUSTOM";
|
||||
}, [currentUserData?.artistNamePreference, currentUserData?.userName]);
|
||||
if (currentUserData?.userName) return true
|
||||
const pref = currentUserData?.artistNamePreference
|
||||
return pref === 'REAL_NAME' || pref === 'CUSTOM'
|
||||
}, [currentUserData?.artistNamePreference, currentUserData?.userName])
|
||||
|
||||
const realName = useMemo(
|
||||
() => buildFullName(currentUserData),
|
||||
[
|
||||
currentUserData?.firstName,
|
||||
currentUserData?.lastName,
|
||||
currentUserData?.displayName,
|
||||
],
|
||||
);
|
||||
[currentUserData?.firstName, currentUserData?.lastName, currentUserData?.displayName]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (pseudoVisible) {
|
||||
setPseudo(currentUserData?.userName || "");
|
||||
setPseudo(currentUserData?.userName || '')
|
||||
}
|
||||
}, [currentUserData?.userName, pseudoVisible]);
|
||||
}, [currentUserData?.userName, pseudoVisible])
|
||||
|
||||
const ensureArtistPreference = useCallback(
|
||||
(nextAction) => {
|
||||
if (hasArtistPreference) {
|
||||
if (typeof nextAction === "function") {
|
||||
nextAction();
|
||||
if (typeof nextAction === 'function') {
|
||||
nextAction()
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
setPendingAction(() => nextAction);
|
||||
setChoiceVisible(true);
|
||||
setPendingAction(() => nextAction)
|
||||
setChoiceVisible(true)
|
||||
},
|
||||
[hasArtistPreference],
|
||||
);
|
||||
[hasArtistPreference]
|
||||
)
|
||||
|
||||
const runPendingAction = useCallback(async () => {
|
||||
if (typeof pendingAction === "function") {
|
||||
const action = pendingAction;
|
||||
setPendingAction(null);
|
||||
await action();
|
||||
if (typeof pendingAction === 'function') {
|
||||
const action = pendingAction
|
||||
setPendingAction(null)
|
||||
await action()
|
||||
} else {
|
||||
setPendingAction(null);
|
||||
setPendingAction(null)
|
||||
}
|
||||
}, [pendingAction]);
|
||||
}, [pendingAction])
|
||||
|
||||
const applyDisplayNameToProjects = useCallback(
|
||||
async (displayName) => {
|
||||
if (!currentUID) return;
|
||||
if (!currentUID) return
|
||||
const safeName =
|
||||
typeof displayName === "string" && displayName.trim().length > 0
|
||||
? displayName.trim()
|
||||
: null;
|
||||
typeof displayName === 'string' && displayName.trim().length > 0 ? displayName.trim() : null
|
||||
try {
|
||||
const snapshot = await projectsRef
|
||||
.where("userId", "==", currentUID)
|
||||
.get();
|
||||
const snapshot = await projectsRef.where('userId', '==', currentUID).get()
|
||||
if (!snapshot.empty) {
|
||||
const batch = firebase.firestore().batch();
|
||||
const batch = firebase.firestore().batch()
|
||||
snapshot.docs.forEach((doc) => {
|
||||
batch.set(doc.ref, { userName: safeName }, { merge: true });
|
||||
});
|
||||
await batch.commit();
|
||||
batch.set(doc.ref, { userName: safeName }, { merge: true })
|
||||
})
|
||||
await batch.commit()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("applyDisplayNameToProjects error", error?.message);
|
||||
console.log('applyDisplayNameToProjects error', error?.message)
|
||||
}
|
||||
|
||||
if (!projectId) return;
|
||||
if (!projectId) return
|
||||
try {
|
||||
await projectsRef
|
||||
.doc(projectId)
|
||||
.set({ userName: safeName }, { merge: true });
|
||||
await projectsRef.doc(projectId).set({ userName: safeName }, { merge: true })
|
||||
} catch (error) {
|
||||
console.log("update current project userName error", error?.message);
|
||||
console.log('update current project userName error', error?.message)
|
||||
}
|
||||
},
|
||||
[currentUID, projectId],
|
||||
);
|
||||
[currentUID, projectId]
|
||||
)
|
||||
|
||||
const handleGenerateCover = useCallback(() => {
|
||||
if (hasFinalCover || hasGeneratedOptions) {
|
||||
navigate(Routes.ValidateCover);
|
||||
return;
|
||||
navigate(Routes.ValidateCover)
|
||||
return
|
||||
}
|
||||
ensureArtistPreference(() => navigate(Routes.PouchReady));
|
||||
}, [ensureArtistPreference, hasFinalCover, hasGeneratedOptions]);
|
||||
ensureArtistPreference(() => navigate(Routes.PouchReady))
|
||||
}, [ensureArtistPreference, hasFinalCover, hasGeneratedOptions])
|
||||
|
||||
const closeChoiceModal = useCallback(() => {
|
||||
setChoiceVisible(false);
|
||||
setPendingAction(null);
|
||||
}, []);
|
||||
setChoiceVisible(false)
|
||||
setPendingAction(null)
|
||||
}, [])
|
||||
|
||||
const openPseudoModal = useCallback(() => {
|
||||
setChoiceVisible(false);
|
||||
setPseudoVisible(true);
|
||||
}, []);
|
||||
setChoiceVisible(false)
|
||||
setPseudoVisible(true)
|
||||
}, [])
|
||||
|
||||
const handleUseRealName = useCallback(async () => {
|
||||
if (!currentUID) return;
|
||||
if (!currentUID) return
|
||||
if (!realName) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
type: 'error',
|
||||
text: "Renseigne ton prénom et nom dans ton profil ou crée un nom d'artiste.",
|
||||
});
|
||||
setChoiceVisible(false);
|
||||
setPseudoVisible(true);
|
||||
return;
|
||||
})
|
||||
setChoiceVisible(false)
|
||||
setPseudoVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSavingChoice(true);
|
||||
setSavingChoice(true)
|
||||
const updates = {
|
||||
artistNamePreference: "REAL_NAME",
|
||||
artistNamePreference: 'REAL_NAME',
|
||||
userName: realName,
|
||||
userNameLower: realName.toLowerCase(),
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
};
|
||||
await usersRef.doc(currentUID).set(updates, { merge: true });
|
||||
await applyDisplayNameToProjects(realName);
|
||||
setTooltip({ type: "success", text: "Nom d'artiste mis à jour" });
|
||||
setChoiceVisible(false);
|
||||
await runPendingAction();
|
||||
}
|
||||
await usersRef.doc(currentUID).set(updates, { merge: true })
|
||||
await applyDisplayNameToProjects(realName)
|
||||
setTooltip({ type: 'success', text: "Nom d'artiste mis à jour" })
|
||||
setChoiceVisible(false)
|
||||
await runPendingAction()
|
||||
} catch (e) {
|
||||
console.log("handleUseRealName error", e?.message);
|
||||
console.log('handleUseRealName error', e?.message)
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: e?.message || "Impossible de mettre à jour le nom",
|
||||
});
|
||||
type: 'error',
|
||||
text: e?.message || 'Impossible de mettre à jour le nom',
|
||||
})
|
||||
} finally {
|
||||
setSavingChoice(false);
|
||||
setSavingChoice(false)
|
||||
}
|
||||
}, [
|
||||
applyDisplayNameToProjects,
|
||||
@@ -175,68 +164,53 @@ export const ChooseCoverType = () => {
|
||||
realName,
|
||||
runPendingAction,
|
||||
setTooltip,
|
||||
]);
|
||||
])
|
||||
|
||||
const handleCancelPseudo = useCallback(() => {
|
||||
setPseudoVisible(false);
|
||||
setPendingAction(null);
|
||||
}, []);
|
||||
setPseudoVisible(false)
|
||||
setPendingAction(null)
|
||||
}, [])
|
||||
|
||||
const handleSavePseudo = useCallback(async () => {
|
||||
const value = (pseudo || "").trim();
|
||||
if (!value || !currentUID) return;
|
||||
const value = (pseudo || '').trim()
|
||||
if (!value || !currentUID) return
|
||||
try {
|
||||
setSavingPseudo(true);
|
||||
const lower = value.toLowerCase();
|
||||
const existing = await usersRef
|
||||
.where("userNameLower", "==", lower)
|
||||
.limit(1)
|
||||
.get();
|
||||
setSavingPseudo(true)
|
||||
const lower = value.toLowerCase()
|
||||
const existing = await usersRef.where('userNameLower', '==', lower).limit(1).get()
|
||||
if (!existing.empty && existing.docs[0].id !== currentUID) {
|
||||
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(currentUID).set(
|
||||
{
|
||||
userName: value,
|
||||
userNameLower: lower,
|
||||
artistNamePreference: "CUSTOM",
|
||||
artistNamePreference: 'CUSTOM',
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
await applyDisplayNameToProjects(value);
|
||||
setTooltip({ type: "success", text: "Pseudo enregistré" });
|
||||
setPseudoVisible(false);
|
||||
await runPendingAction();
|
||||
{ merge: true }
|
||||
)
|
||||
await applyDisplayNameToProjects(value)
|
||||
setTooltip({ type: 'success', text: 'Pseudo enregistré' })
|
||||
setPseudoVisible(false)
|
||||
await runPendingAction()
|
||||
} catch (e) {
|
||||
console.log("handleSavePseudo error", e?.message);
|
||||
console.log('handleSavePseudo error', e?.message)
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: e?.message || "Enregistrement impossible",
|
||||
});
|
||||
type: 'error',
|
||||
text: e?.message || 'Enregistrement impossible',
|
||||
})
|
||||
} finally {
|
||||
setSavingPseudo(false);
|
||||
setSavingPseudo(false)
|
||||
}
|
||||
}, [
|
||||
applyDisplayNameToProjects,
|
||||
currentUID,
|
||||
pseudo,
|
||||
runPendingAction,
|
||||
setTooltip,
|
||||
]);
|
||||
}, [applyDisplayNameToProjects, currentUID, pseudo, runPendingAction, setTooltip])
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.studioBG2}
|
||||
backgroundColor={"#2A2E33"}
|
||||
headerType="NONE"
|
||||
>
|
||||
<Page backgroundImg={background.studioBG2} backgroundColor={'#2A2E33'} headerType="NONE">
|
||||
<MusicLandHeader onPressBack={goBack} progress={9} />
|
||||
<View
|
||||
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
|
||||
>
|
||||
<View style={{ flex: 1, position: 'relative', justifyContent: 'flex-end' }}>
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: gutters * 2,
|
||||
@@ -249,9 +223,7 @@ export const ChooseCoverType = () => {
|
||||
onPress={handlePickUserImage}
|
||||
/> */}
|
||||
<GradientButton
|
||||
title={
|
||||
hasFinalCover ? "Pochette déjà validée" : "Générer la pochette"
|
||||
}
|
||||
title={hasFinalCover ? 'Pochette déjà validée' : 'Générer la pochette'}
|
||||
onPress={handleGenerateCover}
|
||||
disabled={hasFinalCover}
|
||||
/>
|
||||
@@ -277,12 +249,11 @@ export const ChooseCoverType = () => {
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.modalTitle}>Nom d'artiste</Text>
|
||||
<Text style={styles.modalDescription}>
|
||||
Veux-tu utiliser ton prénom et nom pour tes musiques ou créer un
|
||||
nom d'artiste ?
|
||||
Veux-tu utiliser ton prénom et nom pour tes musiques ou créer un nom d'artiste ?
|
||||
</Text>
|
||||
<View style={styles.modalButtons}>
|
||||
<BorderGradientButton
|
||||
title={savingChoice ? "Chargement..." : "Utiliser prénom/nom"}
|
||||
title={savingChoice ? 'Chargement...' : 'Utiliser prénom/nom'}
|
||||
onPress={handleUseRealName}
|
||||
disabled={savingChoice}
|
||||
/>
|
||||
@@ -333,9 +304,9 @@ export const ChooseCoverType = () => {
|
||||
disabled={savingPseudo}
|
||||
/>
|
||||
<GradientButton
|
||||
title={savingPseudo ? "Enregistrement..." : "Valider"}
|
||||
title={savingPseudo ? 'Enregistrement...' : 'Valider'}
|
||||
onPress={handleSavePseudo}
|
||||
disabled={savingPseudo || !(pseudo || "").trim()}
|
||||
disabled={savingPseudo || !(pseudo || '').trim()}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -343,25 +314,25 @@ export const ChooseCoverType = () => {
|
||||
</View>
|
||||
</Modal>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.65)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: 'rgba(0,0,0,0.65)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: gutters,
|
||||
},
|
||||
modalOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
modalCard: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
maxWidth: 420,
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
overflow: 'hidden',
|
||||
backgroundColor: Palette.glass,
|
||||
},
|
||||
modalContent: {
|
||||
@@ -373,26 +344,26 @@ const styles = StyleSheet.create({
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalDescription: {
|
||||
fontSize: 15,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalWarning: {
|
||||
fontSize: 13,
|
||||
color: Palette.orange,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
},
|
||||
modalButtons: {
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
width: '80%',
|
||||
alignSelf: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
inputWrapper: {
|
||||
gap: 12,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,82 +1,74 @@
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Text, View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import loaderMessages from "../../config/loaderMessages";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUserData } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette, Style } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import ProgressBar from "../../components/ProgressBar";
|
||||
const COVER_PROGRESS_MAX = 98;
|
||||
const COVER_PROGRESS_INTERVAL_MS = 500;
|
||||
const COVER_FAKE_DURATION_MS = 2 * 60 * 1000;
|
||||
import { Image as ExpoImage } from 'expo-image'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ActivityIndicator, Text, View } from 'react-native'
|
||||
import { background } from '../../assets'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import loaderMessages from '../../config/loaderMessages'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette, Style } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import ProgressBar from '../../components/ProgressBar'
|
||||
const COVER_PROGRESS_MAX = 98
|
||||
const COVER_PROGRESS_INTERVAL_MS = 500
|
||||
const COVER_FAKE_DURATION_MS = 2 * 60 * 1000
|
||||
|
||||
const PhotoCover = () => {
|
||||
const [coverProgress, setCoverProgress] = useState(0);
|
||||
const progressIntervalRef = useRef(null);
|
||||
const progressStartRef = useRef(null);
|
||||
const { selectedProject } = useUserData();
|
||||
const isGenerating = selectedProject?.coverStatus === "GENERATING";
|
||||
const coverGenerationMessage = isWeb
|
||||
? loaderMessages.photoCoverGenerationWeb
|
||||
: "";
|
||||
const [coverProgress, setCoverProgress] = useState(0)
|
||||
const progressIntervalRef = useRef(null)
|
||||
const progressStartRef = useRef(null)
|
||||
const { selectedProject } = useUserData()
|
||||
const isGenerating = selectedProject?.coverStatus === 'GENERATING'
|
||||
const coverGenerationMessage = isWeb ? loaderMessages.photoCoverGenerationWeb : ''
|
||||
|
||||
const coverOptions = Array.isArray(selectedProject?.cover?.options)
|
||||
? selectedProject.cover.options
|
||||
: [];
|
||||
: []
|
||||
const coverUrl =
|
||||
selectedProject?.cover?.result ||
|
||||
selectedProject?.cover?.generatedBackground ||
|
||||
coverOptions?.[0]?.finalUrl ||
|
||||
coverOptions?.[0]?.generatedUrl ||
|
||||
null;
|
||||
const coverProgressValue = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round(coverProgress)),
|
||||
);
|
||||
null
|
||||
const coverProgressValue = Math.max(0, Math.min(100, Math.round(coverProgress)))
|
||||
|
||||
useEffect(() => {
|
||||
const clearProgressInterval = () => {
|
||||
if (progressIntervalRef.current) {
|
||||
global.clearInterval(progressIntervalRef.current);
|
||||
progressIntervalRef.current = null;
|
||||
global.clearInterval(progressIntervalRef.current)
|
||||
progressIntervalRef.current = null
|
||||
}
|
||||
};
|
||||
|
||||
if (!isGenerating || coverUrl) {
|
||||
clearProgressInterval();
|
||||
progressStartRef.current = null;
|
||||
setCoverProgress(coverUrl ? 100 : 0);
|
||||
return clearProgressInterval;
|
||||
}
|
||||
|
||||
progressStartRef.current = new Date();
|
||||
setCoverProgress(0);
|
||||
clearProgressInterval();
|
||||
progressIntervalRef.current = global.setInterval(() => {
|
||||
const start = progressStartRef.current;
|
||||
if (!start) return;
|
||||
const elapsed = Date.now() - start.getTime();
|
||||
const ratio = Math.max(
|
||||
0,
|
||||
Math.min(1, elapsed / COVER_FAKE_DURATION_MS),
|
||||
);
|
||||
const next = COVER_PROGRESS_MAX * ratio;
|
||||
setCoverProgress((prev) => {
|
||||
if (prev >= COVER_PROGRESS_MAX) return COVER_PROGRESS_MAX;
|
||||
return next >= COVER_PROGRESS_MAX ? COVER_PROGRESS_MAX : next;
|
||||
});
|
||||
}, COVER_PROGRESS_INTERVAL_MS);
|
||||
if (!isGenerating || coverUrl) {
|
||||
clearProgressInterval()
|
||||
progressStartRef.current = null
|
||||
setCoverProgress(coverUrl ? 100 : 0)
|
||||
return clearProgressInterval
|
||||
}
|
||||
|
||||
return clearProgressInterval;
|
||||
}, [coverUrl, isGenerating]);
|
||||
progressStartRef.current = new Date()
|
||||
setCoverProgress(0)
|
||||
clearProgressInterval()
|
||||
progressIntervalRef.current = global.setInterval(() => {
|
||||
const start = progressStartRef.current
|
||||
if (!start) return
|
||||
const elapsed = Date.now() - start.getTime()
|
||||
const ratio = Math.max(0, Math.min(1, elapsed / COVER_FAKE_DURATION_MS))
|
||||
const next = COVER_PROGRESS_MAX * ratio
|
||||
setCoverProgress((prev) => {
|
||||
if (prev >= COVER_PROGRESS_MAX) return COVER_PROGRESS_MAX
|
||||
return next >= COVER_PROGRESS_MAX ? COVER_PROGRESS_MAX : next
|
||||
})
|
||||
}, COVER_PROGRESS_INTERVAL_MS)
|
||||
|
||||
return clearProgressInterval
|
||||
}, [coverUrl, isGenerating])
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
@@ -87,7 +79,7 @@ const PhotoCover = () => {
|
||||
subTitle="Le logo est automatiquement ajouté en bas à droite."
|
||||
/>
|
||||
<View style={{ flex: 1, ...Style.containerCenter }}>
|
||||
<View style={{ width: "80%", position: "relative" }}>
|
||||
<View style={{ width: '80%', position: 'relative' }}>
|
||||
{coverUrl && !isGenerating ? (
|
||||
<ExpoImage
|
||||
source={{ uri: coverUrl }}
|
||||
@@ -96,23 +88,23 @@ const PhotoCover = () => {
|
||||
contentFit="cover"
|
||||
transition={120}
|
||||
style={{
|
||||
width: isWeb ? 300 : "100%",
|
||||
width: isWeb ? 300 : '100%',
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
alignSelf: "center",
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#00000040",
|
||||
backgroundColor: '#00000040',
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
<View style={{ alignItems: "center", gap: 8 }}>
|
||||
<View style={{ alignItems: 'center', gap: 8 }}>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
@@ -121,7 +113,7 @@ const PhotoCover = () => {
|
||||
style={{
|
||||
color: Palette.white,
|
||||
marginTop: 6,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
@@ -132,14 +124,14 @@ const PhotoCover = () => {
|
||||
style={{
|
||||
color: Palette.white,
|
||||
marginTop: 6,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
Génération en cours.
|
||||
</Text>
|
||||
)}
|
||||
<View style={{ alignItems: "center", gap: 10 }}>
|
||||
<View style={{ alignItems: 'center', gap: 10 }}>
|
||||
<ProgressBar gradient progress={coverProgressValue} />
|
||||
<Text
|
||||
style={{
|
||||
@@ -157,12 +149,11 @@ const PhotoCover = () => {
|
||||
style={{
|
||||
color: Palette.white,
|
||||
marginTop: 6,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
La pochette sera disponible une fois la génération
|
||||
terminée.
|
||||
La pochette sera disponible une fois la génération terminée.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@@ -174,8 +165,8 @@ const PhotoCover = () => {
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: gutters * 2,
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
width: '80%',
|
||||
alignSelf: 'center',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
@@ -186,7 +177,7 @@ const PhotoCover = () => {
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PhotoCover;
|
||||
export default PhotoCover
|
||||
|
||||
+243
-311
@@ -1,12 +1,6 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { BlurView } from 'expo-blur'
|
||||
import { Image as ExpoImage } from 'expo-image'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
@@ -17,241 +11,211 @@ import {
|
||||
View,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { background, icons } from "../../assets";
|
||||
import alert from "../../components/Alert";
|
||||
import AppCheckbox from "../../components/AppCheckbox";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import firebase, { tasksRef } from "../../config/firebase";
|
||||
import loaderMessages from "../../config/loaderMessages";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import useGlobalLoading from "../../hooks/useGlobalLoading";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette, Style } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { getStageAction } from "../../utils/projectStages";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import ProgressBar from "../../components/ProgressBar";
|
||||
} from 'react-native'
|
||||
import { background, icons } from '../../assets'
|
||||
import alert from '../../components/Alert'
|
||||
import AppCheckbox from '../../components/AppCheckbox'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import firebase, { tasksRef } from '../../config/firebase'
|
||||
import loaderMessages from '../../config/loaderMessages'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import useGlobalLoading from '../../hooks/useGlobalLoading'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUser } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette, Style } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import { getStageAction } from '../../utils/projectStages'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import ProgressBar from '../../components/ProgressBar'
|
||||
|
||||
const COVER_STYLE_PRESETS = [
|
||||
"Cyberpunk",
|
||||
"Dessins animé",
|
||||
"Dessins animé rétro",
|
||||
"Portrait théâtralisé",
|
||||
"Livre de coloriage",
|
||||
"Shooting",
|
||||
];
|
||||
const COVER_PROGRESS_MAX = 98;
|
||||
const COVER_PROGRESS_INTERVAL_MS = 500;
|
||||
const COVER_FAKE_DURATION_MS = 2 * 60 * 1000;
|
||||
'Cyberpunk',
|
||||
'Dessins animé',
|
||||
'Dessins animé rétro',
|
||||
'Portrait théâtralisé',
|
||||
'Livre de coloriage',
|
||||
'Shooting',
|
||||
]
|
||||
const COVER_PROGRESS_MAX = 98
|
||||
const COVER_PROGRESS_INTERVAL_MS = 500
|
||||
const COVER_FAKE_DURATION_MS = 2 * 60 * 1000
|
||||
|
||||
const PouchReady = () => {
|
||||
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||
const { setLoading } = useGlobalLoading();
|
||||
const isGenerating = selectedProject?.coverStatus === "GENERATING";
|
||||
const { selectedProjectId, selectedProject, updateProjectData } = useUser()
|
||||
const { setLoading } = useGlobalLoading()
|
||||
const isGenerating = selectedProject?.coverStatus === 'GENERATING'
|
||||
const coverOptions = useMemo(() => {
|
||||
if (!Array.isArray(selectedProject?.cover?.options)) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return selectedProject.cover.options.filter(Boolean);
|
||||
}, [selectedProject?.cover?.options]);
|
||||
const hasGeneratedOptions = coverOptions.length > 0;
|
||||
const selectedOptionId = selectedProject?.cover?.selectedOptionId || null;
|
||||
return selectedProject.cover.options.filter(Boolean)
|
||||
}, [selectedProject?.cover?.options])
|
||||
const hasGeneratedOptions = coverOptions.length > 0
|
||||
const selectedOptionId = selectedProject?.cover?.selectedOptionId || null
|
||||
const selectedOption = useMemo(() => {
|
||||
if (!coverOptions.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const found = coverOptions.find(
|
||||
(option) => option?.id === selectedOptionId,
|
||||
);
|
||||
return found || coverOptions[0] || null;
|
||||
}, [coverOptions, selectedOptionId]);
|
||||
const coverBackgroundMessage = isWeb
|
||||
? loaderMessages.pouchReadyGenerationWeb
|
||||
: "";
|
||||
const found = coverOptions.find((option) => option?.id === selectedOptionId)
|
||||
return found || coverOptions[0] || null
|
||||
}, [coverOptions, selectedOptionId])
|
||||
const coverBackgroundMessage = isWeb ? loaderMessages.pouchReadyGenerationWeb : ''
|
||||
const generationLoadingMessage = useMemo(() => {
|
||||
if (typeof coverBackgroundMessage === "string") {
|
||||
const trimmedMessage = coverBackgroundMessage.trim();
|
||||
if (typeof coverBackgroundMessage === 'string') {
|
||||
const trimmedMessage = coverBackgroundMessage.trim()
|
||||
if (trimmedMessage.length) {
|
||||
return trimmedMessage;
|
||||
return trimmedMessage
|
||||
}
|
||||
}
|
||||
return "Nous lançons la génération de ta pochette...";
|
||||
}, [coverBackgroundMessage]);
|
||||
return 'Nous lançons la génération de ta pochette...'
|
||||
}, [coverBackgroundMessage])
|
||||
|
||||
const [styleMode, setStyleMode] = useState("preset");
|
||||
const [selectedPresetStyle, setSelectedPresetStyle] = useState(
|
||||
COVER_STYLE_PRESETS[0],
|
||||
);
|
||||
const [customStyle, setCustomStyle] = useState("");
|
||||
const [isPresetDropdownOpen, setIsPresetDropdownOpen] = useState(false);
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [isAwaitingGenerationStart, setIsAwaitingGenerationStart] =
|
||||
useState(false);
|
||||
const [coverProgress, setCoverProgress] = useState(0);
|
||||
const coverProgressIntervalRef = useRef(null);
|
||||
const coverProgressStartRef = useRef(null);
|
||||
const [styleMode, setStyleMode] = useState('preset')
|
||||
const [selectedPresetStyle, setSelectedPresetStyle] = useState(COVER_STYLE_PRESETS[0])
|
||||
const [customStyle, setCustomStyle] = useState('')
|
||||
const [isPresetDropdownOpen, setIsPresetDropdownOpen] = useState(false)
|
||||
const [isSelecting, setIsSelecting] = useState(false)
|
||||
const [isAwaitingGenerationStart, setIsAwaitingGenerationStart] = useState(false)
|
||||
const [coverProgress, setCoverProgress] = useState(0)
|
||||
const coverProgressIntervalRef = useRef(null)
|
||||
const coverProgressStartRef = useRef(null)
|
||||
|
||||
const showGenerationLoading = useCallback(async () => {
|
||||
setIsAwaitingGenerationStart(true);
|
||||
await setLoading(true, { message: generationLoadingMessage });
|
||||
}, [generationLoadingMessage, setLoading]);
|
||||
setIsAwaitingGenerationStart(true)
|
||||
await setLoading(true, { message: generationLoadingMessage })
|
||||
}, [generationLoadingMessage, setLoading])
|
||||
|
||||
useEffect(() => {
|
||||
const projectStyle = (selectedProject?.coverStyle || "").trim();
|
||||
const isPresetStyle = COVER_STYLE_PRESETS.includes(projectStyle);
|
||||
const projectStyle = (selectedProject?.coverStyle || '').trim()
|
||||
const isPresetStyle = COVER_STYLE_PRESETS.includes(projectStyle)
|
||||
|
||||
if (!projectStyle) {
|
||||
setStyleMode("preset");
|
||||
setSelectedPresetStyle(COVER_STYLE_PRESETS[0]);
|
||||
setCustomStyle("");
|
||||
setIsPresetDropdownOpen(false);
|
||||
return;
|
||||
setStyleMode('preset')
|
||||
setSelectedPresetStyle(COVER_STYLE_PRESETS[0])
|
||||
setCustomStyle('')
|
||||
setIsPresetDropdownOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isPresetStyle) {
|
||||
setStyleMode("preset");
|
||||
setSelectedPresetStyle(projectStyle);
|
||||
setCustomStyle("");
|
||||
setStyleMode('preset')
|
||||
setSelectedPresetStyle(projectStyle)
|
||||
setCustomStyle('')
|
||||
} else {
|
||||
setStyleMode("custom");
|
||||
setCustomStyle(projectStyle);
|
||||
setStyleMode('custom')
|
||||
setCustomStyle(projectStyle)
|
||||
}
|
||||
setIsPresetDropdownOpen(false);
|
||||
}, [selectedProject?.coverStyle]);
|
||||
setIsPresetDropdownOpen(false)
|
||||
}, [selectedProject?.coverStyle])
|
||||
|
||||
const generateCover = useCallback(
|
||||
async (styleValue) => {
|
||||
if (!selectedProjectId) return;
|
||||
if (!selectedProjectId) return
|
||||
if (hasGeneratedOptions) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const trimmedStyle = String(styleValue || "").trim();
|
||||
const trimmedStyle = String(styleValue || '').trim()
|
||||
if (!trimmedStyle) {
|
||||
alert("Attention", "Merci de renseigner un style pour la pochette.", [
|
||||
{ text: "OK" },
|
||||
]);
|
||||
return;
|
||||
alert('Attention', 'Merci de renseigner un style pour la pochette.', [{ text: 'OK' }])
|
||||
return
|
||||
}
|
||||
try {
|
||||
await showGenerationLoading();
|
||||
await showGenerationLoading()
|
||||
await updateProjectData(
|
||||
{
|
||||
coverStyle: trimmedStyle,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
await tasksRef.add({
|
||||
type: "cover",
|
||||
type: 'cover',
|
||||
projectId: selectedProjectId,
|
||||
status: "PENDING",
|
||||
status: 'PENDING',
|
||||
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("Cover task error", e?.message);
|
||||
setIsAwaitingGenerationStart(false);
|
||||
await setLoading(false);
|
||||
console.log('Cover task error', e?.message)
|
||||
setIsAwaitingGenerationStart(false)
|
||||
await setLoading(false)
|
||||
}
|
||||
},
|
||||
[
|
||||
hasGeneratedOptions,
|
||||
selectedProjectId,
|
||||
setLoading,
|
||||
showGenerationLoading,
|
||||
updateProjectData,
|
||||
],
|
||||
);
|
||||
[hasGeneratedOptions, selectedProjectId, setLoading, showGenerationLoading, updateProjectData]
|
||||
)
|
||||
|
||||
const requestCoverGeneration = useCallback(() => {
|
||||
const selectedStyle =
|
||||
styleMode === "preset" ? selectedPresetStyle : customStyle;
|
||||
const trimmedStyle = (selectedStyle || "").trim();
|
||||
const selectedStyle = styleMode === 'preset' ? selectedPresetStyle : customStyle
|
||||
const trimmedStyle = (selectedStyle || '').trim()
|
||||
if (!trimmedStyle) {
|
||||
alert("Attention", "Merci de renseigner un style pour la pochette.", [
|
||||
{ text: "OK" },
|
||||
]);
|
||||
return;
|
||||
alert('Attention', 'Merci de renseigner un style pour la pochette.', [{ text: 'OK' }])
|
||||
return
|
||||
}
|
||||
generateCover(trimmedStyle);
|
||||
}, [customStyle, generateCover, selectedPresetStyle, styleMode]);
|
||||
generateCover(trimmedStyle)
|
||||
}, [customStyle, generateCover, selectedPresetStyle, styleMode])
|
||||
|
||||
const coverPreviewUrl =
|
||||
selectedProject?.cover?.result ||
|
||||
selectedProject?.cover?.generatedBackground ||
|
||||
null;
|
||||
selectedProject?.cover?.result || selectedProject?.cover?.generatedBackground || null
|
||||
|
||||
const shouldShowStylePanel =
|
||||
!isGenerating && !hasGeneratedOptions && !coverPreviewUrl;
|
||||
const shouldShowStylePanel = !isGenerating && !hasGeneratedOptions && !coverPreviewUrl
|
||||
|
||||
const displayOptions = hasGeneratedOptions
|
||||
? coverOptions
|
||||
: coverPreviewUrl
|
||||
? [
|
||||
{
|
||||
id: "preview",
|
||||
id: 'preview',
|
||||
finalUrl: selectedProject?.cover?.result || null,
|
||||
generatedUrl: coverPreviewUrl,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const isCoverLoading =
|
||||
isGenerating && !hasGeneratedOptions && !coverPreviewUrl;
|
||||
const coverProgressValue = Math.max(
|
||||
0,
|
||||
Math.min(100, Math.round(coverProgress)),
|
||||
);
|
||||
: []
|
||||
const isCoverLoading = isGenerating && !hasGeneratedOptions && !coverPreviewUrl
|
||||
const coverProgressValue = Math.max(0, Math.min(100, Math.round(coverProgress)))
|
||||
|
||||
useEffect(() => {
|
||||
const clearProgressInterval = () => {
|
||||
if (coverProgressIntervalRef.current) {
|
||||
global.clearInterval(coverProgressIntervalRef.current);
|
||||
coverProgressIntervalRef.current = null;
|
||||
global.clearInterval(coverProgressIntervalRef.current)
|
||||
coverProgressIntervalRef.current = null
|
||||
}
|
||||
};
|
||||
|
||||
if (!isCoverLoading) {
|
||||
clearProgressInterval();
|
||||
coverProgressStartRef.current = null;
|
||||
setCoverProgress(hasGeneratedOptions || coverPreviewUrl ? 100 : 0);
|
||||
return clearProgressInterval;
|
||||
}
|
||||
|
||||
coverProgressStartRef.current = new Date();
|
||||
setCoverProgress(0);
|
||||
clearProgressInterval();
|
||||
coverProgressIntervalRef.current = global.setInterval(() => {
|
||||
const start = coverProgressStartRef.current;
|
||||
if (!start) return;
|
||||
const elapsed = Date.now() - start.getTime();
|
||||
const ratio = Math.max(0, Math.min(1, elapsed / COVER_FAKE_DURATION_MS));
|
||||
const next = COVER_PROGRESS_MAX * ratio;
|
||||
setCoverProgress((prev) => {
|
||||
if (prev >= COVER_PROGRESS_MAX) return COVER_PROGRESS_MAX;
|
||||
return next >= COVER_PROGRESS_MAX ? COVER_PROGRESS_MAX : next;
|
||||
});
|
||||
}, COVER_PROGRESS_INTERVAL_MS);
|
||||
if (!isCoverLoading) {
|
||||
clearProgressInterval()
|
||||
coverProgressStartRef.current = null
|
||||
setCoverProgress(hasGeneratedOptions || coverPreviewUrl ? 100 : 0)
|
||||
return clearProgressInterval
|
||||
}
|
||||
|
||||
return clearProgressInterval;
|
||||
}, [isCoverLoading, hasGeneratedOptions, coverPreviewUrl]);
|
||||
coverProgressStartRef.current = new Date()
|
||||
setCoverProgress(0)
|
||||
clearProgressInterval()
|
||||
coverProgressIntervalRef.current = global.setInterval(() => {
|
||||
const start = coverProgressStartRef.current
|
||||
if (!start) return
|
||||
const elapsed = Date.now() - start.getTime()
|
||||
const ratio = Math.max(0, Math.min(1, elapsed / COVER_FAKE_DURATION_MS))
|
||||
const next = COVER_PROGRESS_MAX * ratio
|
||||
setCoverProgress((prev) => {
|
||||
if (prev >= COVER_PROGRESS_MAX) return COVER_PROGRESS_MAX
|
||||
return next >= COVER_PROGRESS_MAX ? COVER_PROGRESS_MAX : next
|
||||
})
|
||||
}, COVER_PROGRESS_INTERVAL_MS)
|
||||
|
||||
return clearProgressInterval
|
||||
}, [isCoverLoading, hasGeneratedOptions, coverPreviewUrl])
|
||||
|
||||
const handleSelectOption = useCallback(
|
||||
async (option) => {
|
||||
if (
|
||||
!option ||
|
||||
option?.id === selectedOptionId ||
|
||||
isSelecting ||
|
||||
!option?.id
|
||||
) {
|
||||
return;
|
||||
if (!option || option?.id === selectedOptionId || isSelecting || !option?.id) {
|
||||
return
|
||||
}
|
||||
const existingCover = selectedProject?.cover || {};
|
||||
const finalUrl = option.finalUrl || option.generatedUrl || null;
|
||||
setIsSelecting(true);
|
||||
const existingCover = selectedProject?.cover || {}
|
||||
const finalUrl = option.finalUrl || option.generatedUrl || null
|
||||
setIsSelecting(true)
|
||||
try {
|
||||
await updateProjectData({
|
||||
cover: {
|
||||
@@ -261,93 +225,85 @@ const PouchReady = () => {
|
||||
result: finalUrl,
|
||||
generatedBackground: option.generatedUrl || option.finalUrl || null,
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("PouchReady: unable to select cover", e?.message);
|
||||
console.log('PouchReady: unable to select cover', e?.message)
|
||||
} finally {
|
||||
setIsSelecting(false);
|
||||
setIsSelecting(false)
|
||||
}
|
||||
},
|
||||
[
|
||||
coverOptions,
|
||||
isSelecting,
|
||||
selectedOptionId,
|
||||
selectedProject?.cover,
|
||||
updateProjectData,
|
||||
],
|
||||
);
|
||||
[coverOptions, isSelecting, selectedOptionId, selectedProject?.cover, updateProjectData]
|
||||
)
|
||||
|
||||
const onValidatePicture = useCallback(() => {
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
navigate(Routes.SongDownload, {
|
||||
project: selectedProject,
|
||||
selectedOption,
|
||||
coverOptions,
|
||||
});
|
||||
}, [coverOptions, navigate, selectedOption, selectedProject]);
|
||||
})
|
||||
}, [coverOptions, navigate, selectedOption, selectedProject])
|
||||
|
||||
const isPrimaryActionDisabled =
|
||||
isGenerating ||
|
||||
(hasGeneratedOptions ? !selectedOption || isSelecting : true);
|
||||
isGenerating || (hasGeneratedOptions ? !selectedOption || isSelecting : true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAwaitingGenerationStart) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (isGenerating) {
|
||||
setIsAwaitingGenerationStart(false);
|
||||
setLoading(false);
|
||||
setIsAwaitingGenerationStart(false)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [isAwaitingGenerationStart, isGenerating, setLoading]);
|
||||
}, [isAwaitingGenerationStart, isGenerating, setLoading])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (isAwaitingGenerationStart) {
|
||||
setIsAwaitingGenerationStart(false);
|
||||
setLoading(false);
|
||||
setIsAwaitingGenerationStart(false)
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[isAwaitingGenerationStart, setLoading],
|
||||
);
|
||||
[isAwaitingGenerationStart, setLoading]
|
||||
)
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<MusicLandHeader onPressBack={goBack} progress={72} />
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<View style={{ flex: 1, marginTop: 0 }}>
|
||||
<CreateLyricsHeader
|
||||
title={
|
||||
selectedProject?.cover?.backgroundGenerated
|
||||
? "Ta pochette est prête!"
|
||||
: "Génération de la pochette"
|
||||
? 'Ta pochette est prête!'
|
||||
: 'Génération de la pochette'
|
||||
}
|
||||
/>
|
||||
<View style={{ flex: 1, ...Style.containerCenter }}>
|
||||
<View style={{ width: isWeb ? "80%" : "80%", gap: 24 }}>
|
||||
<View style={{ width: isWeb ? '80%' : '80%', gap: 24 }}>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: isWeb ? "row" : "column",
|
||||
width: '100%',
|
||||
flexDirection: isWeb ? 'row' : 'column',
|
||||
gap: 16,
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{displayOptions.length > 0 ? (
|
||||
displayOptions.map((option, index) => {
|
||||
const optionUri =
|
||||
option?.finalUrl || option?.generatedUrl || "";
|
||||
if (!optionUri) return null;
|
||||
const optionUri = option?.finalUrl || option?.generatedUrl || ''
|
||||
if (!optionUri) return null
|
||||
if (hasGeneratedOptions) {
|
||||
const isSelected = option?.id === selectedOption?.id;
|
||||
const isSelected = option?.id === selectedOption?.id
|
||||
const cardWidthStyle = isWeb
|
||||
? styles.coverOptionCardWeb
|
||||
: styles.coverOptionCardMobile;
|
||||
: styles.coverOptionCardMobile
|
||||
return (
|
||||
<Pressable
|
||||
key={option?.id || index}
|
||||
@@ -375,22 +331,18 @@ const PouchReady = () => {
|
||||
</View>
|
||||
{isSelected && (
|
||||
<View style={styles.coverOptionSelectedBadge}>
|
||||
<Text style={styles.coverOptionSelectedLabel}>
|
||||
Sélectionnée
|
||||
</Text>
|
||||
<Text style={styles.coverOptionSelectedLabel}>Sélectionnée</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View
|
||||
key={option?.id || index}
|
||||
style={[
|
||||
styles.coverPreviewCard,
|
||||
isWeb
|
||||
? styles.coverPreviewCardWeb
|
||||
: styles.coverPreviewCardMobile,
|
||||
isWeb ? styles.coverPreviewCardWeb : styles.coverPreviewCardMobile,
|
||||
]}
|
||||
>
|
||||
<ExpoImage
|
||||
@@ -402,20 +354,20 @@ const PouchReady = () => {
|
||||
style={styles.coverPreviewImage}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
{isGenerating && (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
width: isWeb ? 300 : "100%",
|
||||
width: isWeb ? 300 : '100%',
|
||||
height: 300,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#00000040",
|
||||
alignSelf: "center",
|
||||
backgroundColor: '#00000040',
|
||||
alignSelf: 'center',
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
@@ -426,7 +378,7 @@ const PouchReady = () => {
|
||||
style={{
|
||||
color: Palette.white,
|
||||
marginTop: 6,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
@@ -438,7 +390,7 @@ const PouchReady = () => {
|
||||
style={{
|
||||
color: Palette.white,
|
||||
marginTop: 6,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
@@ -488,75 +440,60 @@ const PouchReady = () => {
|
||||
<View
|
||||
style={[
|
||||
styles.modeCard,
|
||||
styleMode === "preset" ? styles.modeCardActive : null,
|
||||
styleMode === 'preset' ? styles.modeCardActive : null,
|
||||
]}
|
||||
>
|
||||
<AppCheckbox
|
||||
label="Choisir un style prédéfini"
|
||||
selected={styleMode === "preset"}
|
||||
selected={styleMode === 'preset'}
|
||||
onPress={() => {
|
||||
setStyleMode("preset");
|
||||
setIsPresetDropdownOpen(false);
|
||||
setStyleMode('preset')
|
||||
setIsPresetDropdownOpen(false)
|
||||
if (!selectedPresetStyle) {
|
||||
setSelectedPresetStyle(COVER_STYLE_PRESETS[0]);
|
||||
setSelectedPresetStyle(COVER_STYLE_PRESETS[0])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{styleMode === "preset" && (
|
||||
{styleMode === 'preset' && (
|
||||
<View style={styles.dropdownArea}>
|
||||
<Pressable
|
||||
style={styles.dropdownTrigger}
|
||||
onPress={() =>
|
||||
setIsPresetDropdownOpen((prev) => !prev)
|
||||
}
|
||||
onPress={() => setIsPresetDropdownOpen((prev) => !prev)}
|
||||
>
|
||||
<Text
|
||||
style={styles.dropdownTriggerLabel}
|
||||
numberOfLines={2}
|
||||
>
|
||||
<Text style={styles.dropdownTriggerLabel} numberOfLines={2}>
|
||||
{selectedPresetStyle}
|
||||
</Text>
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
style={[
|
||||
styles.dropdownArrow,
|
||||
isPresetDropdownOpen
|
||||
? styles.dropdownArrowOpen
|
||||
: null,
|
||||
isPresetDropdownOpen ? styles.dropdownArrowOpen : null,
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
{isPresetDropdownOpen && (
|
||||
<View style={styles.dropdownList}>
|
||||
{COVER_STYLE_PRESETS.map((styleOption, index) => {
|
||||
const isActive =
|
||||
selectedPresetStyle === styleOption;
|
||||
const isLast =
|
||||
index === COVER_STYLE_PRESETS.length - 1;
|
||||
const isActive = selectedPresetStyle === styleOption
|
||||
const isLast = index === COVER_STYLE_PRESETS.length - 1
|
||||
return (
|
||||
<Pressable
|
||||
key={styleOption}
|
||||
style={[
|
||||
styles.dropdownOption,
|
||||
!isLast
|
||||
? styles.dropdownOptionDivider
|
||||
: null,
|
||||
isActive
|
||||
? styles.dropdownOptionActive
|
||||
: null,
|
||||
!isLast ? styles.dropdownOptionDivider : null,
|
||||
isActive ? styles.dropdownOptionActive : null,
|
||||
]}
|
||||
onPress={() => {
|
||||
setSelectedPresetStyle(styleOption);
|
||||
setIsPresetDropdownOpen(false);
|
||||
setSelectedPresetStyle(styleOption)
|
||||
setIsPresetDropdownOpen(false)
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dropdownOptionLabel,
|
||||
isActive
|
||||
? styles.dropdownOptionLabelActive
|
||||
: null,
|
||||
isActive ? styles.dropdownOptionLabelActive : null,
|
||||
]}
|
||||
>
|
||||
{styleOption}
|
||||
@@ -569,7 +506,7 @@ const PouchReady = () => {
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
@@ -579,19 +516,19 @@ const PouchReady = () => {
|
||||
<View
|
||||
style={[
|
||||
styles.modeCard,
|
||||
styleMode === "custom" ? styles.modeCardActive : null,
|
||||
styleMode === 'custom' ? styles.modeCardActive : null,
|
||||
]}
|
||||
>
|
||||
<AppCheckbox
|
||||
label="Style personnalisé"
|
||||
selected={styleMode === "custom"}
|
||||
selected={styleMode === 'custom'}
|
||||
onPress={() => {
|
||||
setStyleMode("custom");
|
||||
setIsPresetDropdownOpen(false);
|
||||
setStyleMode('custom')
|
||||
setIsPresetDropdownOpen(false)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{styleMode === "custom" && (
|
||||
{styleMode === 'custom' && (
|
||||
<View style={styles.customInputWrapper}>
|
||||
<TextInput
|
||||
placeholder="Exemple : Collage rétro futuriste lumineux"
|
||||
@@ -618,50 +555,45 @@ const PouchReady = () => {
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: gutters * 2,
|
||||
width: isWeb ? "80%" : "100%",
|
||||
alignSelf: "center",
|
||||
width: isWeb ? '80%' : '100%',
|
||||
alignSelf: 'center',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{!hasGeneratedOptions && (
|
||||
<BorderGradientButton
|
||||
title={
|
||||
isGenerating ? "Génération en cours..." : "Générer la pochette"
|
||||
}
|
||||
title={isGenerating ? 'Génération en cours...' : 'Générer la pochette'}
|
||||
icon={icons.stars}
|
||||
onPress={requestCoverGeneration}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
)}
|
||||
{hasGeneratedOptions && (
|
||||
<GradientButton
|
||||
title={"Valider la pochette"}
|
||||
onPress={onValidatePicture}
|
||||
/>
|
||||
<GradientButton title={'Valider la pochette'} onPress={onValidatePicture} />
|
||||
)}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PouchReady;
|
||||
export default PouchReady
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
coverOptionCard: {
|
||||
position: "relative",
|
||||
position: 'relative',
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.12)",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "rgba(15,12,20,0.4)",
|
||||
borderColor: 'rgba(255,255,255,0.12)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(15,12,20,0.4)',
|
||||
},
|
||||
coverOptionCardWeb: {
|
||||
width: 280,
|
||||
maxWidth: 320,
|
||||
},
|
||||
coverOptionCardMobile: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
},
|
||||
coverOptionCardSelected: {
|
||||
borderColor: Palette.primary,
|
||||
@@ -670,11 +602,11 @@ const styles = StyleSheet.create({
|
||||
opacity: 0.85,
|
||||
},
|
||||
coverOptionImage: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
aspectRatio: 1,
|
||||
},
|
||||
coverOptionBadge: {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
backgroundColor: Palette.transparentBlack,
|
||||
@@ -688,7 +620,7 @@ const styles = StyleSheet.create({
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
coverOptionSelectedBadge: {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
backgroundColor: Palette.primary,
|
||||
@@ -702,16 +634,16 @@ const styles = StyleSheet.create({
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
coverPreviewCard: {
|
||||
alignItems: "center",
|
||||
alignItems: 'center',
|
||||
},
|
||||
coverPreviewCardWeb: {
|
||||
width: 300,
|
||||
},
|
||||
coverPreviewCardMobile: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
},
|
||||
coverPreviewImage: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
height: 260,
|
||||
borderRadius: 20,
|
||||
},
|
||||
@@ -721,9 +653,9 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 20,
|
||||
borderRadius: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
backgroundColor: "rgba(15, 12, 20, 0.78)",
|
||||
overflow: "hidden",
|
||||
borderColor: 'rgba(255,255,255,0.08)',
|
||||
backgroundColor: 'rgba(15, 12, 20, 0.78)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
stylePanelWeb: {
|
||||
paddingHorizontal: 26,
|
||||
@@ -746,32 +678,32 @@ const styles = StyleSheet.create({
|
||||
gap: 18,
|
||||
},
|
||||
modeCard: {
|
||||
alignSelf: "stretch",
|
||||
alignSelf: 'stretch',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
backgroundColor: "rgba(15,12,20,0.6)",
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: 'rgba(15,12,20,0.6)',
|
||||
},
|
||||
modeCardActive: {
|
||||
borderColor: Palette.primary,
|
||||
backgroundColor: "rgba(251,104,168,0.12)",
|
||||
backgroundColor: 'rgba(251,104,168,0.12)',
|
||||
},
|
||||
dropdownArea: {
|
||||
alignSelf: "stretch",
|
||||
alignSelf: 'stretch',
|
||||
gap: 12,
|
||||
},
|
||||
dropdownTrigger: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
backgroundColor: "rgba(15,12,20,0.65)",
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: 'rgba(15,12,20,0.65)',
|
||||
},
|
||||
dropdownTriggerLabel: {
|
||||
flex: 1,
|
||||
@@ -788,30 +720,30 @@ const styles = StyleSheet.create({
|
||||
dropdownArrowOpen: {
|
||||
transform: [
|
||||
{
|
||||
rotate: "180deg",
|
||||
rotate: '180deg',
|
||||
},
|
||||
],
|
||||
},
|
||||
dropdownList: {
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
backgroundColor: "rgba(12,10,18,0.95)",
|
||||
overflow: "hidden",
|
||||
borderColor: 'rgba(255,255,255,0.08)',
|
||||
backgroundColor: 'rgba(12,10,18,0.95)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
dropdownOption: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
dropdownOptionDivider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "rgba(255,255,255,0.06)",
|
||||
borderBottomColor: 'rgba(255,255,255,0.06)',
|
||||
},
|
||||
dropdownOptionActive: {
|
||||
backgroundColor: "rgba(251,104,168,0.12)",
|
||||
backgroundColor: 'rgba(251,104,168,0.12)',
|
||||
},
|
||||
dropdownOptionLabel: {
|
||||
flex: 1,
|
||||
@@ -834,7 +766,7 @@ const styles = StyleSheet.create({
|
||||
minWidth: 240,
|
||||
},
|
||||
coverProgressWrapper: {
|
||||
alignItems: "center",
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
width: 220,
|
||||
},
|
||||
@@ -842,15 +774,15 @@ const styles = StyleSheet.create({
|
||||
width: 260,
|
||||
},
|
||||
coverProgressBar: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
},
|
||||
customInputWrapper: {
|
||||
alignSelf: "stretch",
|
||||
alignSelf: 'stretch',
|
||||
borderRadius: 18,
|
||||
padding: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
backgroundColor: "rgba(15,12,20,0.4)",
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: 'rgba(15,12,20,0.4)',
|
||||
},
|
||||
customInput: {
|
||||
minHeight: 55,
|
||||
@@ -859,7 +791,7 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 12,
|
||||
fontSize: 15,
|
||||
color: Palette.white,
|
||||
backgroundColor: "rgba(15,12,20,0.85)",
|
||||
backgroundColor: 'rgba(15,12,20,0.85)',
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
+214
-251
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Image as ExpoImage } from 'expo-image'
|
||||
import {
|
||||
Linking,
|
||||
Platform,
|
||||
@@ -9,73 +9,70 @@ import {
|
||||
Text,
|
||||
View,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import AppCheckbox from "../../components/AppCheckbox";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation/Routes";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette, Style } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { background } from "../../assets";
|
||||
import { getStageAction } from "../../utils/projectStages";
|
||||
import ClubAdvantagesCard from "../Profile/components/ClubAdvantagesCard";
|
||||
import useGlobalLoading from "../../hooks/useGlobalLoading";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import { getArtistDisplayName } from "../../utils/artistName";
|
||||
import { toDate } from "../../utils/dateFormatting";
|
||||
import SubscriptionConfirmModal from "../../components/SubscriptionConfirmModal";
|
||||
} from 'react-native'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import AppCheckbox from '../../components/AppCheckbox'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation/Routes'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUser } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette, Style } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import { background } from '../../assets'
|
||||
import { getStageAction } from '../../utils/projectStages'
|
||||
import ClubAdvantagesCard from '../Profile/components/ClubAdvantagesCard'
|
||||
import useGlobalLoading from '../../hooks/useGlobalLoading'
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import { getArtistDisplayName } from '../../utils/artistName'
|
||||
import { toDate } from '../../utils/dateFormatting'
|
||||
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
|
||||
|
||||
const SongDownload = ({ route }) => {
|
||||
const {
|
||||
project: routeProject,
|
||||
selectedOption: routeSelectedOption,
|
||||
coverOptions: routeCoverOptions,
|
||||
} = route?.params || {};
|
||||
const { selectedProject, updateProjectData, hasActiveSubscription } =
|
||||
useUser();
|
||||
const { setTooltip } = useMinuit();
|
||||
const { setLoading } = useGlobalLoading();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
|
||||
} = route?.params || {}
|
||||
const { selectedProject, updateProjectData, hasActiveSubscription } = useUser()
|
||||
const { setTooltip } = useMinuit()
|
||||
const { setLoading } = useGlobalLoading()
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
||||
|
||||
const projectForStage = useMemo(
|
||||
() => routeProject || selectedProject || null,
|
||||
[routeProject, selectedProject],
|
||||
);
|
||||
[routeProject, selectedProject]
|
||||
)
|
||||
|
||||
const coverOptions = useMemo(() => {
|
||||
if (Array.isArray(routeCoverOptions) && routeCoverOptions.length) {
|
||||
return routeCoverOptions.filter(Boolean);
|
||||
return routeCoverOptions.filter(Boolean)
|
||||
}
|
||||
if (Array.isArray(projectForStage?.cover?.options)) {
|
||||
return projectForStage.cover.options.filter(Boolean);
|
||||
return projectForStage.cover.options.filter(Boolean)
|
||||
}
|
||||
return [];
|
||||
}, [projectForStage?.cover?.options, routeCoverOptions]);
|
||||
return []
|
||||
}, [projectForStage?.cover?.options, routeCoverOptions])
|
||||
|
||||
const selectedOption = useMemo(() => {
|
||||
if (routeSelectedOption) {
|
||||
return routeSelectedOption;
|
||||
return routeSelectedOption
|
||||
}
|
||||
const selectedId =
|
||||
projectForStage?.cover?.selectedOptionId ||
|
||||
projectForStage?.cover?.selectedOption?.id ||
|
||||
null;
|
||||
projectForStage?.cover?.selectedOptionId || projectForStage?.cover?.selectedOption?.id || null
|
||||
if (selectedId && coverOptions.length) {
|
||||
const match = coverOptions.find((option) => option?.id === selectedId);
|
||||
if (match) return match;
|
||||
const match = coverOptions.find((option) => option?.id === selectedId)
|
||||
if (match) return match
|
||||
}
|
||||
return coverOptions[0] || null;
|
||||
}, [coverOptions, projectForStage?.cover, routeSelectedOption]);
|
||||
return coverOptions[0] || null
|
||||
}, [coverOptions, projectForStage?.cover, routeSelectedOption])
|
||||
|
||||
const coverUrl =
|
||||
selectedOption?.finalUrl ||
|
||||
@@ -83,46 +80,44 @@ const SongDownload = ({ route }) => {
|
||||
projectForStage?.coverUrl ||
|
||||
projectForStage?.cover?.result ||
|
||||
projectForStage?.cover?.generatedBackground ||
|
||||
null;
|
||||
null
|
||||
const trackTitle =
|
||||
typeof projectForStage?.title === "string" && projectForStage.title.trim()
|
||||
typeof projectForStage?.title === 'string' && projectForStage.title.trim()
|
||||
? projectForStage.title.trim()
|
||||
: "Musicland Track";
|
||||
: 'Musicland Track'
|
||||
|
||||
const continueFlow = useCallback(async () => {
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
try {
|
||||
await setLoading(true, { message: "Sauvegarde de ta pochette..." });
|
||||
const existingCover = projectForStage?.cover || {};
|
||||
const finalUrl =
|
||||
selectedOption.finalUrl || selectedOption.generatedUrl || coverUrl;
|
||||
await setLoading(true, { message: 'Sauvegarde de ta pochette...' })
|
||||
const existingCover = projectForStage?.cover || {}
|
||||
const finalUrl = selectedOption.finalUrl || selectedOption.generatedUrl || coverUrl
|
||||
const nextCoverData = {
|
||||
...existingCover,
|
||||
options: coverOptions,
|
||||
selectedOptionId: selectedOption.id,
|
||||
result: finalUrl,
|
||||
generatedBackground:
|
||||
selectedOption.generatedUrl || selectedOption.finalUrl || null,
|
||||
};
|
||||
generatedBackground: selectedOption.generatedUrl || selectedOption.finalUrl || null,
|
||||
}
|
||||
await updateProjectData({
|
||||
cover: nextCoverData,
|
||||
coverUrl: finalUrl,
|
||||
});
|
||||
})
|
||||
const nextProject = {
|
||||
...projectForStage,
|
||||
cover: nextCoverData,
|
||||
coverUrl: finalUrl,
|
||||
};
|
||||
const nextPlaybackStage = getStageAction("director", nextProject);
|
||||
const targetRoute = nextPlaybackStage?.route || Routes.Playback;
|
||||
const params = nextPlaybackStage?.params || { project: nextProject };
|
||||
navigate(targetRoute, params);
|
||||
}
|
||||
const nextPlaybackStage = getStageAction('director', nextProject)
|
||||
const targetRoute = nextPlaybackStage?.route || Routes.Playback
|
||||
const params = nextPlaybackStage?.params || { project: nextProject }
|
||||
navigate(targetRoute, params)
|
||||
} catch (error) {
|
||||
console.log("[SongDownload] continue error", error?.message);
|
||||
console.log('[SongDownload] continue error', error?.message)
|
||||
} finally {
|
||||
await setLoading(false);
|
||||
await setLoading(false)
|
||||
}
|
||||
}, [
|
||||
coverOptions,
|
||||
@@ -132,29 +127,29 @@ const SongDownload = ({ route }) => {
|
||||
selectedOption,
|
||||
setLoading,
|
||||
updateProjectData,
|
||||
]);
|
||||
])
|
||||
|
||||
const handleContinue = useCallback(() => {
|
||||
if (!hasAcceptedPublication) {
|
||||
if (setTooltip) {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: "Confirme la diffusion sur Musicland et YouTube avant de continuer",
|
||||
});
|
||||
type: 'error',
|
||||
text: 'Confirme la diffusion sur Musicland et YouTube avant de continuer',
|
||||
})
|
||||
} else {
|
||||
Alert.alert(
|
||||
"Confirmation requise",
|
||||
"Confirme la diffusion sur Musicland et YouTube avant de continuer"
|
||||
);
|
||||
'Confirmation requise',
|
||||
'Confirme la diffusion sur Musicland et YouTube avant de continuer'
|
||||
)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (hasActiveSubscription) {
|
||||
continueFlow();
|
||||
return;
|
||||
continueFlow()
|
||||
return
|
||||
}
|
||||
setShowConfirmModal(true);
|
||||
}, [continueFlow, hasAcceptedPublication, hasActiveSubscription, setTooltip]);
|
||||
setShowConfirmModal(true)
|
||||
}, [continueFlow, hasAcceptedPublication, hasActiveSubscription, setTooltip])
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const downloadUrl =
|
||||
@@ -162,226 +157,208 @@ const SongDownload = ({ route }) => {
|
||||
projectForStage?.playbackUrl ||
|
||||
selectedOption?.finalUrl ||
|
||||
selectedOption?.generatedUrl ||
|
||||
null;
|
||||
null
|
||||
if (!downloadUrl || isDownloading) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
await setLoading(true, { message: "Préparation du téléchargement..." });
|
||||
const artist = getArtistDisplayName(projectForStage, "MusicLand");
|
||||
const createdDate = toDate(projectForStage?.createdAt) || new Date();
|
||||
const createdLabel = createdDate
|
||||
? createdDate.toISOString().split("T")[0]
|
||||
: "";
|
||||
await setLoading(true, { message: 'Préparation du téléchargement...' })
|
||||
const artist = getArtistDisplayName(projectForStage, 'MusicLand')
|
||||
const createdDate = toDate(projectForStage?.createdAt) || new Date()
|
||||
const createdLabel = createdDate ? createdDate.toISOString().split('T')[0] : ''
|
||||
|
||||
const triggerWebDownload = async (url, title) => {
|
||||
setIsDownloading(true);
|
||||
await setLoading(true, { message: "Préparation du téléchargement..." });
|
||||
setIsDownloading(true)
|
||||
await setLoading(true, { message: 'Préparation du téléchargement...' })
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`download_failed_${response.status}`);
|
||||
throw new Error(`download_failed_${response.status}`)
|
||||
}
|
||||
const contentType =
|
||||
response.headers.get("content-type") || "audio/mpeg";
|
||||
const contentType = response.headers.get('content-type') || 'audio/mpeg'
|
||||
const baseName =
|
||||
String(trackTitle || "musicland-track")
|
||||
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||
.trim() || "musicland-track";
|
||||
const filename = `${baseName}.mp3`;
|
||||
const buffer = await response.arrayBuffer();
|
||||
String(trackTitle || 'musicland-track')
|
||||
.replace(/[\\/:*?"<>|]+/g, '-')
|
||||
.trim() || 'musicland-track'
|
||||
const filename = `${baseName}.mp3`
|
||||
const buffer = await response.arrayBuffer()
|
||||
|
||||
if (contentType.includes("audio")) {
|
||||
if (contentType.includes('audio')) {
|
||||
const toSynchSafe = (size) => {
|
||||
const out = new Uint8Array(4);
|
||||
out[0] = (size >> 21) & 0x7f;
|
||||
out[1] = (size >> 14) & 0x7f;
|
||||
out[2] = (size >> 7) & 0x7f;
|
||||
out[3] = size & 0x7f;
|
||||
return out;
|
||||
};
|
||||
const out = new Uint8Array(4)
|
||||
out[0] = (size >> 21) & 0x7f
|
||||
out[1] = (size >> 14) & 0x7f
|
||||
out[2] = (size >> 7) & 0x7f
|
||||
out[3] = size & 0x7f
|
||||
return out
|
||||
}
|
||||
|
||||
const concatBytes = (...arrays) => {
|
||||
const totalLength = arrays.reduce(
|
||||
(sum, arr) => sum + arr.length,
|
||||
0,
|
||||
);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
|
||||
const result = new Uint8Array(totalLength)
|
||||
let offset = 0
|
||||
arrays.forEach((arr) => {
|
||||
result.set(arr, offset);
|
||||
offset += arr.length;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
result.set(arr, offset)
|
||||
offset += arr.length
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const buildTextFrame = (id, value) => {
|
||||
const encoder = new TextEncoder();
|
||||
const textBytes = encoder.encode(value || "");
|
||||
const data = concatBytes(new Uint8Array([0x03]), textBytes);
|
||||
const encoder = new TextEncoder()
|
||||
const textBytes = encoder.encode(value || '')
|
||||
const data = concatBytes(new Uint8Array([0x03]), textBytes)
|
||||
const header = concatBytes(
|
||||
new TextEncoder().encode(id),
|
||||
toSynchSafe(data.length),
|
||||
new Uint8Array([0x00, 0x00]),
|
||||
);
|
||||
return concatBytes(header, data);
|
||||
};
|
||||
new Uint8Array([0x00, 0x00])
|
||||
)
|
||||
return concatBytes(header, data)
|
||||
}
|
||||
|
||||
const buildApicFrame = (imageBytes, mime) => {
|
||||
if (!imageBytes) return null;
|
||||
const encoder = new TextEncoder();
|
||||
const mimeBytes = encoder.encode(mime || "image/jpeg");
|
||||
if (!imageBytes) return null
|
||||
const encoder = new TextEncoder()
|
||||
const mimeBytes = encoder.encode(mime || 'image/jpeg')
|
||||
const data = concatBytes(
|
||||
new Uint8Array([0x03]),
|
||||
mimeBytes,
|
||||
new Uint8Array([0x00]), // mime terminator
|
||||
new Uint8Array([0x03]), // front cover
|
||||
new Uint8Array([0x00]), // empty description
|
||||
new Uint8Array(imageBytes),
|
||||
);
|
||||
new Uint8Array(imageBytes)
|
||||
)
|
||||
const header = concatBytes(
|
||||
new TextEncoder().encode("APIC"),
|
||||
new TextEncoder().encode('APIC'),
|
||||
toSynchSafe(data.length),
|
||||
new Uint8Array([0x00, 0x00]),
|
||||
);
|
||||
return concatBytes(header, data);
|
||||
};
|
||||
new Uint8Array([0x00, 0x00])
|
||||
)
|
||||
return concatBytes(header, data)
|
||||
}
|
||||
|
||||
const buildId3Tag = (audioBytes, coverBytes, coverMime) => {
|
||||
const frames = [];
|
||||
frames.push(buildTextFrame("TIT2", trackTitle));
|
||||
frames.push(buildTextFrame("TPE1", artist));
|
||||
frames.push(buildTextFrame("TDRC", createdLabel));
|
||||
const apic = buildApicFrame(coverBytes, coverMime);
|
||||
if (apic) frames.push(apic);
|
||||
const frames = []
|
||||
frames.push(buildTextFrame('TIT2', trackTitle))
|
||||
frames.push(buildTextFrame('TPE1', artist))
|
||||
frames.push(buildTextFrame('TDRC', createdLabel))
|
||||
const apic = buildApicFrame(coverBytes, coverMime)
|
||||
if (apic) frames.push(apic)
|
||||
|
||||
const framesData = concatBytes(...frames);
|
||||
const framesData = concatBytes(...frames)
|
||||
const header = concatBytes(
|
||||
new TextEncoder().encode("ID3"),
|
||||
new TextEncoder().encode('ID3'),
|
||||
new Uint8Array([0x04, 0x00]), // version 2.4.0
|
||||
new Uint8Array([0x00]), // flags
|
||||
toSynchSafe(framesData.length),
|
||||
);
|
||||
return concatBytes(header, framesData, audioBytes);
|
||||
};
|
||||
toSynchSafe(framesData.length)
|
||||
)
|
||||
return concatBytes(header, framesData, audioBytes)
|
||||
}
|
||||
|
||||
const fetchCoverBytes = async () => {
|
||||
if (!coverUrl) return { bytes: null, mime: null };
|
||||
if (!coverUrl) return { bytes: null, mime: null }
|
||||
try {
|
||||
const res = await fetch(coverUrl);
|
||||
const mime = res.headers?.get("content-type") || "image/jpeg";
|
||||
const bufferImage = await res.arrayBuffer();
|
||||
return { bytes: new Uint8Array(bufferImage), mime };
|
||||
const res = await fetch(coverUrl)
|
||||
const mime = res.headers?.get('content-type') || 'image/jpeg'
|
||||
const bufferImage = await res.arrayBuffer()
|
||||
return { bytes: new Uint8Array(bufferImage), mime }
|
||||
} catch {
|
||||
return { bytes: null, mime: null };
|
||||
return { bytes: null, mime: null }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { bytes: coverBytes, mime: coverMime } =
|
||||
await fetchCoverBytes();
|
||||
const merged = buildId3Tag(
|
||||
new Uint8Array(buffer),
|
||||
coverBytes,
|
||||
coverMime,
|
||||
);
|
||||
const blob = new Blob([merged], { type: contentType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = blobUrl;
|
||||
downloadLink.download = filename;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
const { bytes: coverBytes, mime: coverMime } = await fetchCoverBytes()
|
||||
const merged = buildId3Tag(new Uint8Array(buffer), coverBytes, coverMime)
|
||||
const blob = new Blob([merged], { type: contentType })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const downloadLink = document.createElement('a')
|
||||
downloadLink.href = blobUrl
|
||||
downloadLink.download = filename
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
document.body.removeChild(downloadLink)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
} else {
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = blobUrl;
|
||||
downloadLink.download = filename;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
const blob = await response.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const downloadLink = document.createElement('a')
|
||||
downloadLink.href = blobUrl
|
||||
downloadLink.download = filename
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
document.body.removeChild(downloadLink)
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
setIsDownloading(false)
|
||||
}
|
||||
};
|
||||
|
||||
if (isWeb) {
|
||||
await triggerWebDownload(downloadUrl, projectForStage?.title);
|
||||
await setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloading(true);
|
||||
if (isWeb) {
|
||||
await triggerWebDownload(downloadUrl, projectForStage?.title)
|
||||
await setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsDownloading(true)
|
||||
try {
|
||||
const baseName =
|
||||
String(trackTitle || "musicland-track")
|
||||
.replace(/[\\/:*?"<>|]+/g, "-")
|
||||
.trim() || "musicland-track";
|
||||
const fileName = `${baseName}.mp3`;
|
||||
const targetUri = `${FileSystem.cacheDirectory || ""}${fileName}`;
|
||||
String(trackTitle || 'musicland-track')
|
||||
.replace(/[\\/:*?"<>|]+/g, '-')
|
||||
.trim() || 'musicland-track'
|
||||
const fileName = `${baseName}.mp3`
|
||||
const targetUri = `${FileSystem.cacheDirectory || ''}${fileName}`
|
||||
|
||||
const downloadResult = await FileSystem.downloadAsync(
|
||||
downloadUrl,
|
||||
targetUri,
|
||||
);
|
||||
const downloadResult = await FileSystem.downloadAsync(downloadUrl, targetUri)
|
||||
if (!downloadResult?.uri) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
if (Platform.OS === 'android') {
|
||||
const permissions =
|
||||
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
|
||||
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync()
|
||||
if (!permissions.granted || !permissions.directoryUri) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const base64 = await FileSystem.readAsStringAsync(downloadResult.uri, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
})
|
||||
try {
|
||||
const destUri =
|
||||
await FileSystem.StorageAccessFramework.createFileAsync(
|
||||
permissions.directoryUri,
|
||||
fileName,
|
||||
"audio/mpeg",
|
||||
);
|
||||
const destUri = await FileSystem.StorageAccessFramework.createFileAsync(
|
||||
permissions.directoryUri,
|
||||
fileName,
|
||||
'audio/mpeg'
|
||||
)
|
||||
await FileSystem.writeAsStringAsync(destUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.log("[SongDownload] SAF write error", error?.message);
|
||||
console.log('[SongDownload] SAF write error', error?.message)
|
||||
}
|
||||
} else {
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
try {
|
||||
await Sharing.shareAsync(downloadResult.uri, {
|
||||
mimeType: "audio/mpeg",
|
||||
dialogTitle: "Enregistrer la musique",
|
||||
});
|
||||
mimeType: 'audio/mpeg',
|
||||
dialogTitle: 'Enregistrer la musique',
|
||||
})
|
||||
} catch (error) {
|
||||
console.log("[SongDownload] share error", error?.message);
|
||||
console.log('[SongDownload] share error', error?.message)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await Linking.openURL(downloadResult.uri);
|
||||
await Linking.openURL(downloadResult.uri)
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"[SongDownload] open local file error",
|
||||
error?.message,
|
||||
);
|
||||
console.log('[SongDownload] open local file error', error?.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[SongDownload] native download open error", error?.message);
|
||||
console.log('[SongDownload] native download open error', error?.message)
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
await setLoading(false);
|
||||
setIsDownloading(false)
|
||||
await setLoading(false)
|
||||
}
|
||||
}, [coverUrl, isDownloading, projectForStage, selectedOption, trackTitle]);
|
||||
}, [coverUrl, isDownloading, projectForStage, selectedOption, trackTitle])
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
@@ -397,11 +374,7 @@ const SongDownload = ({ route }) => {
|
||||
<CreateLyricsHeader title="Ta pochette est validée" />
|
||||
<View style={styles.coverRow}>
|
||||
{coverUrl ? (
|
||||
<ExpoImage
|
||||
source={{ uri: coverUrl }}
|
||||
style={styles.coverImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
||||
) : (
|
||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||
@@ -409,14 +382,8 @@ const SongDownload = ({ route }) => {
|
||||
)}
|
||||
|
||||
<Pressable style={styles.downloadTile} onPress={handleDownload}>
|
||||
<MaterialCommunityIcons
|
||||
name="download"
|
||||
size={22}
|
||||
color={Palette.white}
|
||||
/>
|
||||
<Text style={styles.downloadText}>
|
||||
Acheter ce morceau pour 1,99€
|
||||
</Text>
|
||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||
<Text style={styles.downloadText}>Acheter ce morceau pour 1,99€</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -434,11 +401,7 @@ const SongDownload = ({ route }) => {
|
||||
</View>
|
||||
|
||||
<BorderGradientButton
|
||||
title={
|
||||
hasActiveSubscription
|
||||
? "Continuer"
|
||||
: "Continuer sans générer de revenus"
|
||||
}
|
||||
title={hasActiveSubscription ? 'Continuer' : 'Continuer sans générer de revenus'}
|
||||
onPress={handleContinue}
|
||||
containerStyle={styles.continueButton}
|
||||
/>
|
||||
@@ -450,13 +413,13 @@ const SongDownload = ({ route }) => {
|
||||
onContinue={continueFlow}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
paddingHorizontal: gutters * 1.2,
|
||||
paddingBottom: gutters * 1.8,
|
||||
paddingTop: gutters,
|
||||
@@ -481,9 +444,9 @@ const styles = StyleSheet.create({
|
||||
opacity: 0.8,
|
||||
},
|
||||
coverRow: {
|
||||
flexDirection: isWeb ? "row" : "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: isWeb ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: gutters * 0.8,
|
||||
},
|
||||
coverImage: {
|
||||
@@ -491,24 +454,24 @@ const styles = StyleSheet.create({
|
||||
height: 150,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.18)",
|
||||
borderColor: 'rgba(255, 255, 255, 0.18)',
|
||||
},
|
||||
coverPlaceholder: {
|
||||
...Style.centered,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.06)',
|
||||
},
|
||||
placeholderText: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
color: Palette.grayMid,
|
||||
},
|
||||
downloadTile: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
paddingVertical: gutters * 0.9,
|
||||
paddingHorizontal: gutters * 1.2,
|
||||
borderRadius: 14,
|
||||
backgroundColor: "#8C4BFF",
|
||||
backgroundColor: '#8C4BFF',
|
||||
borderWidth: 0,
|
||||
},
|
||||
downloadText: {
|
||||
@@ -522,6 +485,6 @@ const styles = StyleSheet.create({
|
||||
continueButton: {
|
||||
marginTop: gutters * 0.5,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default SongDownload;
|
||||
export default SongDownload
|
||||
|
||||
@@ -1,60 +1,58 @@
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { background } from "../../assets";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser, useUserData } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette, Style } from "../../styles";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||
import { Image as ExpoImage } from 'expo-image'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import { background } from '../../assets'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUser, useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette, Style } from '../../styles'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
|
||||
const ValidateCover = () => {
|
||||
const { selectedProject, updateProjectData } = useUserData();
|
||||
const { videos } = useUser();
|
||||
const benhaiUrl = isWeb ? videos?.benhaiWeb || null : videos?.benhai;
|
||||
const [showIntro, setShowIntro] = useState(null);
|
||||
const { selectedProject, updateProjectData } = useUserData()
|
||||
const { videos } = useUser()
|
||||
const benhaiUrl = isWeb ? videos?.benhaiWeb || null : videos?.benhai
|
||||
const [showIntro, setShowIntro] = useState(null)
|
||||
|
||||
const { setIsLoading } = useMinuit();
|
||||
const { setIsLoading } = useMinuit()
|
||||
|
||||
const coverOptions = useMemo(() => {
|
||||
if (!Array.isArray(selectedProject?.cover?.options)) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return selectedProject.cover.options.filter((option) => option);
|
||||
}, [selectedProject]);
|
||||
return selectedProject.cover.options.filter((option) => option)
|
||||
}, [selectedProject])
|
||||
|
||||
const selectedOptionId = selectedProject?.cover?.selectedOptionId || null;
|
||||
const selectedOptionId = selectedProject?.cover?.selectedOptionId || null
|
||||
const selectedOption = useMemo(() => {
|
||||
if (!coverOptions.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const found = coverOptions.find(
|
||||
(option) => option?.id === selectedOptionId
|
||||
);
|
||||
return found || coverOptions[0] || null;
|
||||
}, [coverOptions, selectedOptionId]);
|
||||
const found = coverOptions.find((option) => option?.id === selectedOptionId)
|
||||
return found || coverOptions[0] || null
|
||||
}, [coverOptions, selectedOptionId])
|
||||
|
||||
const isMultipleOptions = coverOptions.length > 1;
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const isMultipleOptions = coverOptions.length > 1
|
||||
const [isSelecting, setIsSelecting] = useState(false)
|
||||
|
||||
async function onStartAgain() {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
await setIsLoading(true)
|
||||
await updateProjectData({
|
||||
cover: null,
|
||||
coverUrl: null,
|
||||
});
|
||||
navigate(Routes.ChooseCoverType);
|
||||
})
|
||||
navigate(Routes.ChooseCoverType)
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e)
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
await setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +64,11 @@ const ValidateCover = () => {
|
||||
isSelecting ||
|
||||
selectedOptionId // Prevent re-selection
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const existingCover = selectedProject?.cover || {};
|
||||
const finalUrl = option.finalUrl || option.generatedUrl || null;
|
||||
setIsSelecting(true);
|
||||
const existingCover = selectedProject?.cover || {}
|
||||
const finalUrl = option.finalUrl || option.generatedUrl || null
|
||||
setIsSelecting(true)
|
||||
try {
|
||||
await updateProjectData({
|
||||
cover: {
|
||||
@@ -80,37 +78,31 @@ const ValidateCover = () => {
|
||||
result: finalUrl,
|
||||
generatedBackground: option.generatedUrl || option.finalUrl || null,
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("ValidateCover: unable to select cover", e?.message);
|
||||
console.log('ValidateCover: unable to select cover', e?.message)
|
||||
} finally {
|
||||
setIsSelecting(false);
|
||||
setIsSelecting(false)
|
||||
}
|
||||
},
|
||||
[
|
||||
coverOptions,
|
||||
isSelecting,
|
||||
selectedOptionId,
|
||||
selectedProject,
|
||||
updateProjectData,
|
||||
]
|
||||
);
|
||||
[coverOptions, isSelecting, selectedOptionId, selectedProject, updateProjectData]
|
||||
)
|
||||
|
||||
async function onValidatePicture() {
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
setShowIntro(benhaiUrl);
|
||||
setShowIntro(benhaiUrl)
|
||||
}
|
||||
|
||||
const handleCloseIntro = useCallback(() => {
|
||||
setShowIntro(null);
|
||||
setShowIntro(null)
|
||||
navigate(Routes.SongDownload, {
|
||||
project: selectedProject,
|
||||
selectedOption,
|
||||
coverOptions,
|
||||
});
|
||||
}, [selectedProject, selectedOption, coverOptions]);
|
||||
})
|
||||
}, [selectedProject, selectedOption, coverOptions])
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
@@ -123,21 +115,20 @@ const ValidateCover = () => {
|
||||
<View style={{ flex: 1, ...Style.containerCenter }}>
|
||||
<View
|
||||
style={{
|
||||
width: "90%",
|
||||
width: '90%',
|
||||
gap: 16,
|
||||
flexDirection: isMultipleOptions ? "row" : "column",
|
||||
flexWrap: isMultipleOptions ? "wrap" : "nowrap",
|
||||
justifyContent: "center",
|
||||
alignItems: isMultipleOptions ? "stretch" : "center",
|
||||
flexDirection: isMultipleOptions ? 'row' : 'column',
|
||||
flexWrap: isMultipleOptions ? 'wrap' : 'nowrap',
|
||||
justifyContent: 'center',
|
||||
alignItems: isMultipleOptions ? 'stretch' : 'center',
|
||||
}}
|
||||
>
|
||||
{coverOptions.map((option, index) => {
|
||||
const isSelected = option?.id === selectedOption?.id;
|
||||
const imageSource =
|
||||
option?.finalUrl || option?.generatedUrl || null;
|
||||
const isSelected = option?.id === selectedOption?.id
|
||||
const imageSource = option?.finalUrl || option?.generatedUrl || null
|
||||
|
||||
if (!imageSource) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -148,19 +139,11 @@ const ValidateCover = () => {
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
borderWidth: isSelected ? 2 : 1,
|
||||
borderColor: isSelected
|
||||
? Palette.primary
|
||||
: Palette.ultraLightWhite,
|
||||
overflow: "hidden",
|
||||
width: isMultipleOptions
|
||||
? isWeb
|
||||
? 280
|
||||
: "48%"
|
||||
: isWeb
|
||||
? 300
|
||||
: "90%",
|
||||
maxWidth: isWeb ? 320 : "100%",
|
||||
position: "relative",
|
||||
borderColor: isSelected ? Palette.primary : Palette.ultraLightWhite,
|
||||
overflow: 'hidden',
|
||||
width: isMultipleOptions ? (isWeb ? 280 : '48%') : isWeb ? 300 : '90%',
|
||||
maxWidth: isWeb ? 320 : '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<ExpoImage
|
||||
@@ -168,11 +151,11 @@ const ValidateCover = () => {
|
||||
cachePolicy="memory-disk"
|
||||
contentFit="cover"
|
||||
transition={120}
|
||||
style={{ width: "100%", aspectRatio: 1 }}
|
||||
style={{ width: '100%', aspectRatio: 1 }}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
backgroundColor: Palette.transparentBlack,
|
||||
@@ -184,7 +167,7 @@ const ValidateCover = () => {
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.white,
|
||||
fontWeight: "600",
|
||||
fontWeight: '600',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
@@ -194,7 +177,7 @@ const ValidateCover = () => {
|
||||
{isSelected && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
backgroundColor: Palette.primary,
|
||||
@@ -206,7 +189,7 @@ const ValidateCover = () => {
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.white,
|
||||
fontWeight: "600",
|
||||
fontWeight: '600',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
@@ -215,18 +198,17 @@ const ValidateCover = () => {
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
)
|
||||
})}
|
||||
{!coverOptions.length && (
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.white,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
Les options de pochette seront disponibles à la fin de la
|
||||
génération.
|
||||
Les options de pochette seront disponibles à la fin de la génération.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@@ -235,8 +217,8 @@ const ValidateCover = () => {
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: gutters * 2,
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
width: '80%',
|
||||
alignSelf: 'center',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
@@ -250,13 +232,9 @@ const ValidateCover = () => {
|
||||
disabled={!selectedOption || isSelecting}
|
||||
/>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
url={showIntro}
|
||||
visible={!!showIntro}
|
||||
onClose={handleCloseIntro}
|
||||
/>
|
||||
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ValidateCover;
|
||||
export default ValidateCover
|
||||
|
||||
Reference in New Issue
Block a user