fix of the day, payment + sub + music generation
This commit is contained in:
+7
-13
@@ -75,7 +75,8 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency);
|
||||
const intervalLabel = getIntervalLabel(plan?.recurring);
|
||||
const coinsPerMonth =
|
||||
typeof plan?.coinsPerMonth === "number" && Number.isFinite(plan.coinsPerMonth)
|
||||
typeof plan?.coinsPerMonth === "number" &&
|
||||
Number.isFinite(plan.coinsPerMonth)
|
||||
? Math.round(plan.coinsPerMonth)
|
||||
: null;
|
||||
const planBadgeKey = getPlanKeyForBadge(plan);
|
||||
@@ -278,7 +279,7 @@ const PLAN_SEGMENTS = [
|
||||
{ key: "annual", label: "Annuel" },
|
||||
];
|
||||
|
||||
const HERO_IMAGE_WIDTH = isWeb ? 1280 : 520;
|
||||
const HERO_IMAGE_WIDTH = isWeb ? 1280 : 700;
|
||||
const HERO_IMAGE_HEIGHT = isWeb ? 760 : 360;
|
||||
const PAGE_BACKGROUND_COLOR = "#303438";
|
||||
const SUBSCRIPTION_DISCLAIMER =
|
||||
@@ -399,11 +400,7 @@ export default function Payments() {
|
||||
} else if (shouldApplyPack && !isCatalogLoading) {
|
||||
initialPackHandledRef.current = true;
|
||||
}
|
||||
}, [
|
||||
normalizedPlans,
|
||||
initialSubscriptionPack,
|
||||
isCatalogLoading,
|
||||
]);
|
||||
}, [normalizedPlans, initialSubscriptionPack, isCatalogLoading]);
|
||||
|
||||
const currentPlans = normalizedPlans[billingPeriod] || [];
|
||||
const selectedPriceId = selectedPriceIds[billingPeriod];
|
||||
@@ -460,8 +457,7 @@ export default function Payments() {
|
||||
}, [billingPeriod]);
|
||||
|
||||
const isProcessing = Boolean(processingPriceId);
|
||||
const isActionDisabled =
|
||||
!selectedPriceId || isProcessing || isLoadingPlans;
|
||||
const isActionDisabled = !selectedPriceId || isProcessing || isLoadingPlans;
|
||||
const actionButtonTitle = isProcessing
|
||||
? "Redirection..."
|
||||
: "Choisir cet abonnement";
|
||||
@@ -621,7 +617,7 @@ export default function Payments() {
|
||||
>
|
||||
<View style={[styles.inner, isMobile && styles.mobileInner]}>
|
||||
<ExpoImage
|
||||
source={backgroundImage}
|
||||
source={background.bgTrans}
|
||||
contentFit="cover"
|
||||
style={styles.centerImage}
|
||||
/>
|
||||
@@ -685,9 +681,7 @@ export default function Payments() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.disclaimer}>
|
||||
{SUBSCRIPTION_DISCLAIMER}
|
||||
</Text>
|
||||
<Text style={styles.disclaimer}>{SUBSCRIPTION_DISCLAIMER}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters } from "../../styles";
|
||||
|
||||
const Playback = ({ route }) => {
|
||||
const Playback = ({ route, navigation }) => {
|
||||
const { project } = route.params || {};
|
||||
const { videos } = useUser();
|
||||
const [showIntro, setShowIntro] = useState(false);
|
||||
@@ -50,7 +50,10 @@ const Playback = ({ route }) => {
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
|
||||
<Image source={ai.john} style={styles.img} resizeMode="contain" />
|
||||
<MusicLandHeader onPressBack={goBack} progress={25} />
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigation.navigate(Routes.Home)}
|
||||
progress={25}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useUserData } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { formatDate, toDate } from "../../utils/dateFormatting";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
|
||||
@@ -183,9 +185,9 @@ const ManageSubscription = ({ navigation }) => {
|
||||
setRemoteError(null);
|
||||
|
||||
try {
|
||||
const callable = getFunctionsClient(
|
||||
FUNCTIONS_REGION,
|
||||
).httpsCallable("subscription-getActiveSubscription");
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||||
"subscription-getActiveSubscription",
|
||||
);
|
||||
const { data } = await callable();
|
||||
if (!isMounted) {
|
||||
return;
|
||||
@@ -232,7 +234,9 @@ const ManageSubscription = ({ navigation }) => {
|
||||
pickString(currentUserData?.stripeSubscriptionStatus) ||
|
||||
pickString(remoteSubscription?.status) ||
|
||||
pickString(currentUserData?.stripeSubscription?.status) ||
|
||||
pickString(currentUserData?.stripeSubscription?.stripeSubscriptionStatus) ||
|
||||
pickString(
|
||||
currentUserData?.stripeSubscription?.stripeSubscriptionStatus,
|
||||
) ||
|
||||
pickString(rawSubscription?.status) ||
|
||||
null;
|
||||
|
||||
@@ -349,8 +353,7 @@ const ManageSubscription = ({ navigation }) => {
|
||||
? computeFirstAnnualGrantFromCreation(createdAtDate)
|
||||
: null;
|
||||
const futureFallbackGrant =
|
||||
fallbackInitialGrant &&
|
||||
fallbackInitialGrant.getTime() >= now.getTime()
|
||||
fallbackInitialGrant && fallbackInitialGrant.getTime() >= now.getTime()
|
||||
? fallbackInitialGrant
|
||||
: null;
|
||||
|
||||
@@ -418,9 +421,9 @@ const ManageSubscription = ({ navigation }) => {
|
||||
setSuccessMessage(null);
|
||||
|
||||
try {
|
||||
const callable = getFunctionsClient(
|
||||
FUNCTIONS_REGION,
|
||||
).httpsCallable("subscription-cancelActiveSubscription");
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||||
"subscription-cancelActiveSubscription",
|
||||
);
|
||||
const payload = subscriptionInfo.subscriptionId
|
||||
? { subscriptionId: subscriptionInfo.subscriptionId }
|
||||
: {};
|
||||
@@ -446,9 +449,7 @@ const ManageSubscription = ({ navigation }) => {
|
||||
error?.message || error,
|
||||
);
|
||||
const message =
|
||||
error?.message ||
|
||||
error?.codeMessage ||
|
||||
"Impossible d'annuler l'abonnement pour le moment.";
|
||||
error?.message || "Impossible d'annuler l'abonnement pour le moment.";
|
||||
setErrorMessage(message);
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
@@ -491,14 +492,17 @@ const ManageSubscription = ({ navigation }) => {
|
||||
const cancelButtonTitle = subscriptionInfo.cancelAtPeriodEnd
|
||||
? "Annulation programmée"
|
||||
: isCancelling
|
||||
? "Annulation..."
|
||||
: "Annuler l'abonnement";
|
||||
? "Annulation..."
|
||||
: "Annuler l'abonnement";
|
||||
|
||||
const cancelTitleColor = subscriptionInfo.cancelAtPeriodEnd
|
||||
? Palette.grayMid
|
||||
: Palette.red;
|
||||
|
||||
const backgroundImage = background.bgTrans;
|
||||
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
|
||||
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
|
||||
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120;
|
||||
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80;
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -506,8 +510,27 @@ const ManageSubscription = ({ navigation }) => {
|
||||
title="Mon abonnement"
|
||||
scrollEnabled
|
||||
backgroundColor={PAGE_BACKGROUND_COLOR}
|
||||
backgroundImg={backgroundImage}
|
||||
// backgroundImg={backgroundImage}
|
||||
>
|
||||
<ExpoImage
|
||||
source={background.bgTrans}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
width: HOME_BACKGROUND_STYLE_WIDTH,
|
||||
height: HOME_BACKGROUND_STYLE_HEIGHT,
|
||||
borderRadius: 22,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
top: isWeb ? "45%" : "50%",
|
||||
left: "50%",
|
||||
transform: [
|
||||
{ translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
|
||||
{ translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
|
||||
],
|
||||
pointerEvents: "none",
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
<View style={styles.container}>
|
||||
{subscriptionInfo.hasAnySubscription ? (
|
||||
<>
|
||||
@@ -645,7 +668,6 @@ const ManageSubscription = ({ navigation }) => {
|
||||
containerStyle={styles.cancelButton}
|
||||
titleStyle={{ color: cancelTitleColor }}
|
||||
/>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.card}>
|
||||
|
||||
@@ -464,16 +464,18 @@ const styles = StyleSheet.create({
|
||||
bottomLoginPrompt: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 30,
|
||||
},
|
||||
bottomFooterText: {
|
||||
fontSize: 14,
|
||||
fontSize: 15,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||
textAlign: "center",
|
||||
},
|
||||
bottomFooterLink: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
color: Palette.white,
|
||||
fontSize: 16,
|
||||
},
|
||||
socialWrapper: {
|
||||
width: "100%",
|
||||
|
||||
@@ -27,6 +27,12 @@ const { width } = Dimensions.get("window");
|
||||
const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const CONFIRM_MODAL_MAX_WIDTH = 540;
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
|
||||
const FIRST_VOICE_STEP_INDEX = 1;
|
||||
const TOTAL_STEPS = 1 + VOICE_SECTION_TITLES.length + 2; // genre + voices + instruments + rhythm
|
||||
const LAST_STEP_INDEX = TOTAL_STEPS - 1;
|
||||
const INSTRUMENT_STEP_INDEX = FIRST_VOICE_STEP_INDEX + VOICE_SECTION_TITLES.length;
|
||||
const RHYTHM_STEP_INDEX = LAST_STEP_INDEX;
|
||||
|
||||
const ComposeSong = () => {
|
||||
const scrollRef = useRef(null);
|
||||
@@ -69,19 +75,19 @@ const ComposeSong = () => {
|
||||
}, [currentUserData?.coins]);
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
return Array.isArray(genres) && genres.length > 0;
|
||||
case 1:
|
||||
// Must select at least one in base category
|
||||
return !!(voice && typeof voice === "object" && voice.BASE);
|
||||
case 2:
|
||||
return Array.isArray(instruments) && instruments.length > 0;
|
||||
case 3:
|
||||
return !!rhythm;
|
||||
default:
|
||||
return true;
|
||||
if (selectedIndex === 0) {
|
||||
return Array.isArray(genres) && genres.length > 0;
|
||||
}
|
||||
if (selectedIndex === FIRST_VOICE_STEP_INDEX) {
|
||||
return !!(voice && typeof voice === "object" && voice.BASE);
|
||||
}
|
||||
if (selectedIndex === INSTRUMENT_STEP_INDEX) {
|
||||
return Array.isArray(instruments) && instruments.length > 0;
|
||||
}
|
||||
if (selectedIndex === RHYTHM_STEP_INDEX) {
|
||||
return !!rhythm;
|
||||
}
|
||||
return true;
|
||||
}, [selectedIndex, genres, voice, instruments, rhythm]);
|
||||
|
||||
const musicConfig = useMemo(() => {
|
||||
@@ -199,24 +205,24 @@ const ComposeSong = () => {
|
||||
};
|
||||
|
||||
const onPressNext = () => {
|
||||
if (selectedIndex === 3) {
|
||||
if (selectedIndex === LAST_STEP_INDEX) {
|
||||
setIsConfirmVisible(true);
|
||||
return;
|
||||
}
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
setProgress(progress + 9);
|
||||
setSelectedIndex((prev) => Math.min(prev + 1, LAST_STEP_INDEX));
|
||||
setProgress((prev) => prev + 9);
|
||||
scrollRef.current.scrollToIndex({
|
||||
index: selectedIndex + 1,
|
||||
index: Math.min(selectedIndex + 1, LAST_STEP_INDEX),
|
||||
animated: true,
|
||||
});
|
||||
};
|
||||
|
||||
const onPressBack = () => {
|
||||
if (selectedIndex > 0) {
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
setProgress(progress - 9);
|
||||
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
||||
setProgress((prev) => Math.max(18, prev - 9));
|
||||
scrollRef.current.scrollToIndex({
|
||||
index: selectedIndex - 1,
|
||||
index: Math.max(selectedIndex - 1, 0),
|
||||
animated: true,
|
||||
});
|
||||
} else {
|
||||
@@ -229,6 +235,58 @@ const ComposeSong = () => {
|
||||
navigate(Routes.SongReady);
|
||||
};
|
||||
|
||||
const swiperSlides = React.Children.toArray([
|
||||
<View
|
||||
key="genre"
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<ChooseGenre selected={genres} setSelected={setGenres} />
|
||||
</View>,
|
||||
...VOICE_SECTION_TITLES.map((section) => (
|
||||
<View
|
||||
key={`voice-${section}`}
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<CustomizeVoice
|
||||
category={section}
|
||||
selected={voice}
|
||||
setSelected={setVoice}
|
||||
/>
|
||||
</View>
|
||||
)),
|
||||
<View
|
||||
key="instruments"
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<ChooseInstruments
|
||||
selected={instruments}
|
||||
setSelected={setInstruments}
|
||||
/>
|
||||
</View>,
|
||||
<View
|
||||
key="rhythm"
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
|
||||
</View>,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<MusicLandHeader
|
||||
@@ -246,57 +304,19 @@ const ComposeSong = () => {
|
||||
ref={scrollRef}
|
||||
disableGesture
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<ChooseGenre selected={genres} setSelected={setGenres} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<CustomizeVoice selected={voice} setSelected={setVoice} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<ChooseInstruments
|
||||
selected={instruments}
|
||||
setSelected={setInstruments}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
width: width,
|
||||
height: containerLayout?.height,
|
||||
paddingHorizontal: gutters,
|
||||
}}
|
||||
>
|
||||
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
|
||||
</View>
|
||||
{swiperSlides}
|
||||
</SwiperFlatList>
|
||||
</View>
|
||||
{selectedIndex !== 4 && (
|
||||
{selectedIndex <= LAST_STEP_INDEX && (
|
||||
<View style={{ gap: 12 }}>
|
||||
{selectedIndex === 3 && isRegenerationFlow && (
|
||||
{selectedIndex === LAST_STEP_INDEX && isRegenerationFlow && (
|
||||
<BorderGradientButton
|
||||
title="Annuler la nouvelle génération"
|
||||
onPress={handleCancelGeneration}
|
||||
/>
|
||||
)}
|
||||
<GradientButton
|
||||
title={selectedIndex === 3 ? "Générer" : "Suivant"}
|
||||
title={selectedIndex === LAST_STEP_INDEX ? "Générer" : "Suivant"}
|
||||
onPress={onPressNext}
|
||||
disabled={!isStepValid}
|
||||
/>
|
||||
|
||||
@@ -32,6 +32,7 @@ const { width: windowWidth } = Dimensions.get("window");
|
||||
const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const CONFIRM_MODAL_MAX_WIDTH = 540;
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
|
||||
|
||||
const ComposeSong = () => {
|
||||
const scrollRef = useRef(null);
|
||||
@@ -71,20 +72,68 @@ const ComposeSong = () => {
|
||||
return 0;
|
||||
}, [currentUserData?.coins]);
|
||||
|
||||
const steps = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "genres",
|
||||
render: () => <ChooseGenre selected={genres} setSelected={setGenres} />,
|
||||
},
|
||||
...VOICE_SECTION_TITLES.map((section) => ({
|
||||
key: `voice-${section}`,
|
||||
render: () => (
|
||||
<CustomizeVoice
|
||||
category={section}
|
||||
selected={voice}
|
||||
setSelected={setVoice}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
{
|
||||
key: "instruments",
|
||||
render: () => (
|
||||
<ChooseInstruments
|
||||
selected={instruments}
|
||||
setSelected={setInstruments}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "rhythm",
|
||||
render: () => (
|
||||
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
|
||||
),
|
||||
},
|
||||
],
|
||||
[genres, voice, instruments, rhythm]
|
||||
);
|
||||
|
||||
const totalSteps = steps.length;
|
||||
const lastStepIndex = totalSteps - 1;
|
||||
const instrumentStepIndex = Math.max(lastStepIndex - 1, 0);
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
return Array.isArray(genres) && genres.length > 0;
|
||||
case 1:
|
||||
return !!(voice && typeof voice === "object" && voice.BASE);
|
||||
case 2:
|
||||
return Array.isArray(instruments) && instruments.length > 0;
|
||||
case 3:
|
||||
return !!rhythm;
|
||||
default:
|
||||
return true;
|
||||
if (selectedIndex === 0) {
|
||||
return Array.isArray(genres) && genres.length > 0;
|
||||
}
|
||||
}, [selectedIndex, genres, voice, instruments, rhythm]);
|
||||
if (selectedIndex === 1) {
|
||||
return !!(voice && typeof voice === "object" && voice.BASE);
|
||||
}
|
||||
if (selectedIndex === instrumentStepIndex) {
|
||||
return Array.isArray(instruments) && instruments.length > 0;
|
||||
}
|
||||
if (selectedIndex === lastStepIndex) {
|
||||
return !!rhythm;
|
||||
}
|
||||
return true;
|
||||
}, [
|
||||
selectedIndex,
|
||||
genres,
|
||||
voice,
|
||||
instruments,
|
||||
rhythm,
|
||||
instrumentStepIndex,
|
||||
lastStepIndex,
|
||||
]);
|
||||
|
||||
const musicConfig = useMemo(() => {
|
||||
let lyricsArr = [];
|
||||
@@ -114,36 +163,6 @@ const ComposeSong = () => {
|
||||
};
|
||||
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
|
||||
|
||||
const steps = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "genres",
|
||||
render: () => <ChooseGenre selected={genres} setSelected={setGenres} />,
|
||||
},
|
||||
{
|
||||
key: "voice",
|
||||
render: () => (
|
||||
<CustomizeVoice selected={voice} setSelected={setVoice} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "instruments",
|
||||
render: () => (
|
||||
<ChooseInstruments
|
||||
selected={instruments}
|
||||
setSelected={setInstruments}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "rhythm",
|
||||
render: () => (
|
||||
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
|
||||
),
|
||||
},
|
||||
],
|
||||
[genres, voice, instruments, rhythm]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const nextProgress = 18 + selectedIndex * 9;
|
||||
|
||||
@@ -1,30 +1,112 @@
|
||||
import { View, StyleSheet } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
||||
import { Palette } from "../../styles";
|
||||
import { VOICE } from "../../data/data";
|
||||
import ListSelection from "../../components/ListSelection/ListSelection";
|
||||
|
||||
const CustomizeVoice = ({ selected = {}, setSelected }) => {
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
const SECTION_INSTRUCTIONS = {
|
||||
BASE: "Choisis ta base",
|
||||
SENSIBILITE: "Choisis ta sensibilité",
|
||||
TECHNIQUE: "Choisis ta technique",
|
||||
};
|
||||
|
||||
// Toggle select: only 1 per category (section.title)
|
||||
const onPressSelect = (category, item) => {
|
||||
if (!setSelected) return;
|
||||
const current = selected && typeof selected === "object" ? selected : {};
|
||||
// If tapping the same item, deselect; otherwise, set new one for the category
|
||||
const next = { ...current };
|
||||
if (current[category] === item) next[category] = null;
|
||||
else next[category] = item;
|
||||
setSelected(next);
|
||||
const normalizeCategory = (value) => {
|
||||
if (typeof value !== "string") return "";
|
||||
return value
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toUpperCase();
|
||||
};
|
||||
|
||||
const selectionToObject = (value) => {
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce((acc, entry) => {
|
||||
if (entry && typeof entry === "object") {
|
||||
const cat = entry.category || entry.title || entry.type;
|
||||
if (!cat) return acc;
|
||||
const val =
|
||||
entry.value ??
|
||||
entry.label ??
|
||||
entry.name ??
|
||||
(typeof entry.description === "string"
|
||||
? entry.description
|
||||
: undefined);
|
||||
if (typeof val === "string") {
|
||||
acc[cat] = val;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const objectToSelectionShape = (value, preferArray = false) => {
|
||||
if (!preferArray) return value;
|
||||
return Object.entries(value || {}).map(([category, val]) => ({
|
||||
category,
|
||||
value: val,
|
||||
}));
|
||||
};
|
||||
|
||||
const CustomizeVoice = ({
|
||||
selected = {},
|
||||
setSelected = () => {},
|
||||
category = "BASE",
|
||||
}) => {
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
const normalizedCategory = normalizeCategory(category);
|
||||
|
||||
const sectionData = useMemo(() => {
|
||||
const fallback = Array.isArray(VOICE) && VOICE.length > 0 ? VOICE[0] : null;
|
||||
if (!Array.isArray(VOICE)) return fallback;
|
||||
const match =
|
||||
VOICE.find(
|
||||
(section) => normalizeCategory(section?.title) === normalizedCategory
|
||||
) || fallback;
|
||||
return match;
|
||||
}, [normalizedCategory]);
|
||||
|
||||
const subtitle =
|
||||
SECTION_INSTRUCTIONS[normalizedCategory] ||
|
||||
"Choisis la voix pour ta chanson";
|
||||
|
||||
const voiceObject = useMemo(
|
||||
() => selectionToObject(selected),
|
||||
[selected]
|
||||
);
|
||||
const sectionKey = sectionData?.title || "BASE";
|
||||
const currentValue =
|
||||
typeof voiceObject?.[sectionKey] === "string"
|
||||
? voiceObject[sectionKey]
|
||||
: null;
|
||||
|
||||
const handleSelectionChange = (value) => {
|
||||
if (typeof setSelected !== "function") return;
|
||||
setSelected((previous) => {
|
||||
const wasArray = Array.isArray(previous);
|
||||
const baseObject = selectionToObject(previous);
|
||||
const nextObject = { ...baseObject };
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
nextObject[sectionKey] = value;
|
||||
} else {
|
||||
delete nextObject[sectionKey];
|
||||
}
|
||||
return objectToSelectionShape(nextObject, wasArray);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, gap: 10, paddingTop: 16 }}>
|
||||
<CreateLyricsHeader
|
||||
title="Personnalise la voix que tu veux pour ta chanson"
|
||||
subTitle="Choisis maximum 1 option par catégorie."
|
||||
subTitle={subtitle}
|
||||
/>
|
||||
<View
|
||||
style={{ flex: 1 }}
|
||||
@@ -33,10 +115,10 @@ const CustomizeVoice = ({ selected = {}, setSelected }) => {
|
||||
<ItemContainer height={containerLayout?.height}>
|
||||
<View style={{ flex: 1, gap: 10 }}>
|
||||
<ListSelection
|
||||
options={VOICE}
|
||||
variant="sectioned"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
options={sectionData?.data || []}
|
||||
variant="simple"
|
||||
selected={currentValue}
|
||||
setSelected={handleSelectionChange}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
itemContainerStyle={styles.itemContainer}
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,16 @@ const GeneratingSong = () => {
|
||||
const askedRef = useRef(false);
|
||||
const callingRef = useRef(false);
|
||||
const localStartRef = useRef(null);
|
||||
const errorAlertShownRef = useRef(false);
|
||||
|
||||
const isFailed = selectedProject?.musicStatus === "FAILED";
|
||||
const musicErrorMessage =
|
||||
selectedProject?.musicError?.message ||
|
||||
selectedProject?.musicError?.status ||
|
||||
"Une erreur est survenue lors de la génération.";
|
||||
const hasRefund =
|
||||
selectedProject?.musicCreditsRefunded === true ||
|
||||
typeof selectedProject?.musicCreditsRefundOrderId === "string";
|
||||
|
||||
// project loaded from provider
|
||||
|
||||
@@ -249,6 +259,28 @@ const GeneratingSong = () => {
|
||||
startMusicGenerationOnce,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFailed) {
|
||||
if (!errorAlertShownRef.current) {
|
||||
errorAlertShownRef.current = true;
|
||||
const refundNotice = hasRefund
|
||||
? "\n\nTes crédits ont été automatiquement remboursés."
|
||||
: "";
|
||||
AppAlert("Génération échouée", `${musicErrorMessage}${refundNotice}`);
|
||||
}
|
||||
} else if (selectedProject?.musicStatus === "GENERATING") {
|
||||
errorAlertShownRef.current = false;
|
||||
}
|
||||
}, [
|
||||
hasRefund,
|
||||
isFailed,
|
||||
musicErrorMessage,
|
||||
selectedProject?.musicStatus,
|
||||
]);
|
||||
|
||||
const progressStatus = isFailed ? "error" : "default";
|
||||
const progressLabel = isFailed ? "Erreur" : `${progress}%`;
|
||||
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||
<MusicLandHeader
|
||||
@@ -303,17 +335,37 @@ const GeneratingSong = () => {
|
||||
Ta musique est{"\n"}en cours de création
|
||||
</Text>
|
||||
<View style={{ alignItems: "center", gap: 16 }}>
|
||||
<ProgressBar gradient progress={progress} />
|
||||
<ProgressBar
|
||||
gradient
|
||||
progress={progress}
|
||||
status={progressStatus}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
color: isFailed ? Palette.red : Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{progress}%
|
||||
{progressLabel}
|
||||
</Text>
|
||||
</View>
|
||||
{isFailed ? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: Palette.red,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
lineHeight: 20,
|
||||
}}
|
||||
>
|
||||
{musicErrorMessage}
|
||||
{hasRefund
|
||||
? "\nTes crédits ont été remboursés automatiquement."
|
||||
: "\nTu peux revenir en arrière pour relancer la génération."}
|
||||
</Text>
|
||||
) : null}
|
||||
<GradientButton
|
||||
title={
|
||||
selectedProject?.musicStatus === "GENERATED"
|
||||
|
||||
+120
-76
@@ -2,11 +2,21 @@
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Image, Modal, Pressable, Text, View } from "react-native";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Modal,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { background, icons } from "../../assets";
|
||||
import alert from "../../components/Alert";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import CreditAmount from "../../components/CreditAmount";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import ValidateModal from "../../components/modal/ValidateModal";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
@@ -233,21 +243,26 @@ const SongReady = () => {
|
||||
marginBottom: responsiveHeight(2),
|
||||
}}
|
||||
/>
|
||||
<View style={{ gap: 16, marginTop: isWeb ? gutters * 3 : 0 }}>
|
||||
{musicUrls.map((url, idx) => (
|
||||
<FlatList
|
||||
style={{ flex: 1 }}
|
||||
data={musicUrls}
|
||||
keyExtractor={(item, idx) => `${item || "song"}-${idx}`}
|
||||
renderItem={({ item, index }) => (
|
||||
<SongOptionCard
|
||||
key={`${url || "song"}-${idx}`}
|
||||
index={idx}
|
||||
url={url}
|
||||
isSelected={selectedIndex === idx}
|
||||
index={index}
|
||||
url={item}
|
||||
isSelected={selectedIndex === index}
|
||||
onSelect={setSelectedIndex}
|
||||
registerPlayer={registerPlayer}
|
||||
onTogglePlayback={handleTogglePlayback}
|
||||
projectId={projectId}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
contentContainerStyle={{ gap: 16, paddingBottom: gutters }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
extraData={selectedIndex}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -510,6 +525,18 @@ const SongOptionCard = ({
|
||||
};
|
||||
|
||||
const RegenerateModal = ({ visible, onClose, onConfirm }) => {
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isCompactWidth = width < 420;
|
||||
const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12);
|
||||
const verticalPadding = Math.max(isCompactWidth ? gutters : gutters * 1.5, 12);
|
||||
const availableWidth = Math.max(width - horizontalPadding * 2, 0);
|
||||
const contentWidth =
|
||||
availableWidth > 0
|
||||
? Math.min(REGENERATE_MODAL_MAX_WIDTH, availableWidth)
|
||||
: undefined;
|
||||
const contentGap = isCompactWidth ? 18 : 24;
|
||||
const buttonGap = isCompactWidth ? 12 : 15;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animationType="fade"
|
||||
@@ -520,60 +547,51 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: gutters,
|
||||
paddingVertical: gutters * 1.5,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
}}
|
||||
>
|
||||
<CreateLyricsHeader
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
maxWidth: REGENERATE_MODAL_MAX_WIDTH,
|
||||
alignSelf: "center",
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 24,
|
||||
gap: 24,
|
||||
<ScrollView
|
||||
bounces={false}
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: horizontalPadding,
|
||||
paddingVertical: verticalPadding,
|
||||
minHeight: height,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={{ gap: 24, alignItems: "center" }}>
|
||||
<View
|
||||
style={{
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Re-générer le morceau ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`}
|
||||
</Text>
|
||||
<CreateLyricsHeader
|
||||
containerStyle={{
|
||||
width: contentWidth ?? "100%",
|
||||
maxWidth: REGENERATE_MODAL_MAX_WIDTH,
|
||||
alignSelf: "center",
|
||||
paddingVertical: contentGap,
|
||||
paddingHorizontal: contentGap,
|
||||
gap: contentGap,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: contentGap, alignItems: "center" }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: isCompactWidth ? 10 : 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 6,
|
||||
paddingHorizontal: isCompactWidth ? 4 : 12,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Re-générer le morceau ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
@@ -582,39 +600,65 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte
|
||||
{`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`}
|
||||
</Text>
|
||||
<CreditAmount
|
||||
value={MUSIC_GENERATION_COIN_COST}
|
||||
iconSize={18}
|
||||
textStyle={{
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte
|
||||
</Text>
|
||||
<CreditAmount
|
||||
value={MUSIC_GENERATION_COIN_COST}
|
||||
iconSize={18}
|
||||
textStyle={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Les crédits seront utilisés lors de l'étape de génération.
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
<View
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
width: isCompactWidth ? "100%" : "85%",
|
||||
alignSelf: "center",
|
||||
gap: buttonGap,
|
||||
}}
|
||||
>
|
||||
Les crédits seront utilisés lors de l'étape de génération.
|
||||
</Text>
|
||||
<GradientButton
|
||||
title="Oui, modifier mes choix"
|
||||
onPress={onConfirm}
|
||||
/>
|
||||
<BorderGradientButton title="Retour" onPress={onClose} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
<GradientButton
|
||||
title="Oui, modifier mes choix"
|
||||
onPress={onConfirm}
|
||||
/>
|
||||
<BorderGradientButton title="Retour" onPress={onClose} />
|
||||
</View>
|
||||
</View>
|
||||
</CreateLyricsHeader>
|
||||
</CreateLyricsHeader>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -30,10 +30,12 @@ import { getStageAction } from "../../utils/projectStages";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
|
||||
const COVER_STYLE_PRESETS = [
|
||||
"Néo-Néon / Cyberpunk",
|
||||
"Photographie Minimaliste & Éditoriale",
|
||||
"Illustration & Collage Surréaliste",
|
||||
"Anti-Design / Maximalisme (Tendance Actuelle)",
|
||||
"Cyberpunk",
|
||||
"Dessins animé",
|
||||
"Dessins animé rétro",
|
||||
"Portrait théâtralisé",
|
||||
"Livre de coloriage",
|
||||
"Shooting",
|
||||
];
|
||||
|
||||
const PouchReady = () => {
|
||||
@@ -301,10 +303,7 @@ const PouchReady = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={isWeb ? background.productionBG2 : background.studioBG}
|
||||
headerType="NONE"
|
||||
>
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<MusicLandHeader onPressBack={goBack} progress={72} />
|
||||
<View style={{ flex: 1, marginTop: 0 }}>
|
||||
<CreateLyricsHeader
|
||||
|
||||
Reference in New Issue
Block a user