diff --git a/src/components/FeatureCarousel/FeatureCarousel.js b/src/components/FeatureCarousel/FeatureCarousel.js index 1c9de9a..4124d16 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.js +++ b/src/components/FeatureCarousel/FeatureCarousel.js @@ -12,9 +12,9 @@ import { View, useWindowDimensions, } from "react-native"; -import PersonaCard from "../cards/PersonaCard/PersonaCard"; import { ai } from "../../assets"; import { getCreationStageStates } from "../../utils/projectStages"; +import PersonaCard from "../cards/PersonaCard/PersonaCard"; const STAGE_CARD_CONTENT = [ { @@ -43,6 +43,8 @@ const STAGE_CARD_CONTENT = [ }, ]; +const WEB_SCROLL_INACTIVE_DELTA = 0.05; + const FeatureCarousel = ({ style, selectedProject, @@ -63,14 +65,14 @@ const FeatureCarousel = ({ ...item, isLocked: stageStates[index]?.isLocked ?? true, })), - [stageStates], + [stageStates] ); const { height: windowHeight } = useWindowDimensions(); const isWeb = Platform.OS === "web"; const [viewportHeight, setViewportHeight] = useState(() => - Math.max(windowHeight, 1), + Math.max(windowHeight, 1) ); const updateSnapHeight = useCallback((height) => { @@ -95,7 +97,7 @@ const FeatureCarousel = ({ const pendingScrollRef = useRef(false); const alignTimeoutRef = useRef(null); const activeIndexRef = useRef( - typeof activeIndex === "number" ? activeIndex : 0, + typeof activeIndex === "number" ? activeIndex : 0 ); const onActiveIndexChangeRef = useRef(onActiveIndexChange); @@ -122,12 +124,12 @@ const FeatureCarousel = ({ } return index; }, - [carouselItems.length], + [carouselItems.length] ); const clearPendingAlignment = useCallback(() => { if (alignTimeoutRef.current != null) { - clearTimeout(alignTimeoutRef.current); + globalThis.clearTimeout(alignTimeoutRef.current); alignTimeoutRef.current = null; } }, []); @@ -151,7 +153,7 @@ const FeatureCarousel = ({ } updateSnapHeight(height); try { - ref.scrollToOffset({ offset: clamped * height, animated: false }); + ref.scrollToOffset({ offset: clamped * height, animated }); } catch (_error) { // Ignore scroll errors when list is not ready yet. } @@ -167,7 +169,7 @@ const FeatureCarousel = ({ pendingScrollRef.current = false; } }, - [clampIndex, isWeb, itemHeight, updateSnapHeight], + [clampIndex, isWeb, itemHeight, updateSnapHeight] ); useEffect(() => { @@ -200,7 +202,23 @@ const FeatureCarousel = ({ updateSnapHeight(height); - const nextIndex = clampIndex(Math.round(offset / height)); + const currentIndex = activeIndexRef.current; + const rawIndex = height ? offset / height : currentIndex; + + let nextIndex = currentIndex; + if (isWeb) { + const delta = rawIndex - currentIndex; + if (Math.abs(delta) > WEB_SCROLL_INACTIVE_DELTA) { + if (Math.abs(delta) <= 1) { + nextIndex = clampIndex(currentIndex + (delta > 0 ? 1 : -1)); + } else { + nextIndex = clampIndex(currentIndex + Math.round(delta)); + } + } + } else { + nextIndex = clampIndex(Math.round(rawIndex)); + } + const hasChanged = nextIndex !== activeIndexRef.current; if (hasChanged) { @@ -212,10 +230,11 @@ const FeatureCarousel = ({ } if (isWeb || hasChanged) { - scrollToIndex(nextIndex, !isWeb, height); + const shouldAnimate = isWeb ? true : !isWeb; + scrollToIndex(nextIndex, shouldAnimate, height); } }, - [clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight], + [clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight] ); const handleScrollEnd = useCallback( @@ -225,7 +244,7 @@ const FeatureCarousel = ({ const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height; alignToOffset(offsetY, layoutHeight); }, - [alignToOffset, clearPendingAlignment], + [alignToOffset, clearPendingAlignment] ); const handleScroll = useCallback( @@ -236,12 +255,12 @@ const FeatureCarousel = ({ const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0; const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height; clearPendingAlignment(); - alignTimeoutRef.current = setTimeout(() => { + alignTimeoutRef.current = globalThis.setTimeout(() => { alignToOffset(offsetY, layoutHeight); alignTimeoutRef.current = null; }, 80); }, - [alignToOffset, clearPendingAlignment, isWeb], + [alignToOffset, clearPendingAlignment, isWeb] ); const handleLayout = useCallback( @@ -251,7 +270,7 @@ const FeatureCarousel = ({ updateSnapHeight(layoutHeight); } }, - [updateSnapHeight], + [updateSnapHeight] ); const keyExtractor = useCallback((item) => item.key, []); @@ -265,7 +284,7 @@ const FeatureCarousel = ({ height={itemHeight} /> ), - [itemHeight], + [itemHeight] ); const getItemLayout = useCallback( @@ -274,7 +293,7 @@ const FeatureCarousel = ({ offset: itemHeight * index, index, }), - [itemHeight], + [itemHeight] ); const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }); @@ -303,7 +322,7 @@ const FeatureCarousel = ({ return undefined; } return carouselItems.map((_, index) => index * itemHeight); - }, [carouselItems.length, isWeb, itemHeight]); + }, [carouselItems, isWeb, itemHeight]); return ( diff --git a/src/data/data.js b/src/data/data.js index c25e94f..8db7d2b 100644 --- a/src/data/data.js +++ b/src/data/data.js @@ -1,3 +1,6 @@ +export const OTHER_OBJECTIVE_OPTION = "Autre objectif"; +export const OTHER_STYLE_OPTION = "Autre style"; + export const GOALS = [ "Envoyer un message universel", "Souhaiter un joyeux anniversaire à un ami", @@ -6,6 +9,7 @@ export const GOALS = [ "Pour un mariage", "Pour mon entreprise", "Envoyer un message politique", + OTHER_OBJECTIVE_OPTION, ]; export const EMOTION_CONVEY = [ @@ -182,6 +186,10 @@ export const SONG_STYLE = [ title: "Melancholic", description: "ambiance mélancolique", }, + { + title: OTHER_STYLE_OPTION, + description: "Décris ton style personnalisé", + }, ]; export const CUSTOM_SONG_STRUCTURE = [ diff --git a/src/screens/Writing/CreateLyricsWithAi.js b/src/screens/Writing/CreateLyricsWithAi.js index c5b3a74..cf95855 100644 --- a/src/screens/Writing/CreateLyricsWithAi.js +++ b/src/screens/Writing/CreateLyricsWithAi.js @@ -5,6 +5,7 @@ import { SwiperFlatList } from "react-native-swiper-flatlist"; import { background } from "../../assets"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; +import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data"; import useLayoutType from "../../hooks/useLayoutType"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; @@ -44,6 +45,18 @@ const CreateLyricsWithAi = () => { const [rhymes, setRhymes] = useState(null); const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain'] + const trimmedOtherObjective = + typeof otherObjective === "string" ? otherObjective.trim() : ""; + const shouldUseOtherObjective = objective === OTHER_OBJECTIVE_OPTION; + const persistedOtherObjective = shouldUseOtherObjective + ? trimmedOtherObjective + : ""; + + const trimmedOtherStyle = + typeof otherStyle === "string" ? otherStyle.trim() : ""; + const shouldUseOtherStyle = style === OTHER_STYLE_OPTION; + const persistedOtherStyle = shouldUseOtherStyle ? trimmedOtherStyle : ""; + // Pré-remplir les états depuis le projet sélectionné si disponibles React.useEffect(() => { if (!selectedProject) return; @@ -52,19 +65,28 @@ const CreateLyricsWithAi = () => { const rawOtherObjective = typeof sel.otherObjective === "string" ? sel.otherObjective : ""; - const hasSavedOtherObjective = rawOtherObjective.trim().length > 0; + const trimmedSavedOtherObjective = rawOtherObjective.trim(); + const hasSavedOtherObjective = trimmedSavedOtherObjective.length > 0; const savedObjective = typeof sel.objective === "string" && sel.objective.trim().length > 0 - ? sel.objective + ? sel.objective.trim() : null; if (!otherObjective && hasSavedOtherObjective) { setOtherObjective(rawOtherObjective); - if (objective !== null) { - setObjective(null); - } - } else if (!hasSavedOtherObjective && objective == null && savedObjective) { - setObjective(savedObjective); + } + + const shouldSelectOtherObjective = + hasSavedOtherObjective && + (!savedObjective || savedObjective === OTHER_OBJECTIVE_OPTION); + const resolvedSavedObjective = shouldSelectOtherObjective + ? OTHER_OBJECTIVE_OPTION + : savedObjective; + + if (resolvedSavedObjective && objective !== resolvedSavedObjective) { + setObjective(resolvedSavedObjective); + } else if (!resolvedSavedObjective && objective !== null) { + setObjective(null); } if (!context && typeof sel.context === "string") setContext(sel.context); @@ -86,19 +108,24 @@ const CreateLyricsWithAi = () => { const rawOtherStyle = typeof sel.otherStyle === "string" ? sel.otherStyle : ""; - const hasSavedOtherStyle = rawOtherStyle.trim().length > 0; + const trimmedSavedOtherStyle = rawOtherStyle.trim(); + const hasSavedOtherStyle = trimmedSavedOtherStyle.length > 0; const savedStyle = typeof sel.style === "string" && sel.style.trim().length > 0 - ? sel.style + ? sel.style.trim() : null; - if (!otherStyle && hasSavedOtherStyle) { setOtherStyle(rawOtherStyle); - if (style !== null) { - setStyle(null); - } - } else if (!hasSavedOtherStyle && style == null && savedStyle) { - setStyle(savedStyle); + } + const shouldSelectOtherStyle = + hasSavedOtherStyle && (!savedStyle || savedStyle === OTHER_STYLE_OPTION); + const resolvedSavedStyle = shouldSelectOtherStyle + ? OTHER_STYLE_OPTION + : savedStyle; + if (resolvedSavedStyle && style !== resolvedSavedStyle) { + setStyle(resolvedSavedStyle); + } else if (!resolvedSavedStyle && style !== null) { + setStyle(null); } if (!audience && typeof sel.audience === "string") setAudience(sel.audience); @@ -154,16 +181,21 @@ const CreateLyricsWithAi = () => { }, [hasLyrics]); const lyricsConfig = useMemo(() => { + const resolvedObjective = shouldUseOtherObjective + ? persistedOtherObjective || undefined + : objective || undefined; + const resolvedStyle = shouldUseOtherStyle + ? persistedOtherStyle || undefined + : style || undefined; + return { - objective: otherObjective?.trim() - ? otherObjective.trim() - : objective || undefined, + objective: resolvedObjective, context: context?.trim() ? context.trim() : undefined, emotion: emotion?.title && emotion?.description ? `${emotion.title} : ${emotion.description}` : undefined, - style: otherStyle?.trim() ? otherStyle.trim() : style || undefined, + style: resolvedStyle, audience: audience?.trim() ? audience.trim() : undefined, structure: (customStructure && @@ -174,12 +206,14 @@ const CreateLyricsWithAi = () => { rhymes: rhymes || undefined, }; }, [ + shouldUseOtherObjective, + persistedOtherObjective, objective, - otherObjective, + shouldUseOtherStyle, + persistedOtherStyle, context, emotion, style, - otherStyle, audience, parsedStructure, rhymes, @@ -191,9 +225,7 @@ const CreateLyricsWithAi = () => { case 0: { const hasObjective = typeof objective === "string" && objective.length > 0; - const hasOther = - typeof otherObjective === "string" && - otherObjective.trim().length > 0; + const hasOther = trimmedOtherObjective.length > 0; return !(hasObjective || hasOther); } case 1: { @@ -209,8 +241,7 @@ const CreateLyricsWithAi = () => { } case 3: { const hasStyle = typeof style === "string" && style.length > 0; - const hasOther = - typeof otherStyle === "string" && otherStyle.trim().length > 0; + const hasOther = trimmedOtherStyle.length > 0; return !(hasStyle || hasOther); } case 4: { @@ -233,11 +264,11 @@ const CreateLyricsWithAi = () => { }, [ selectedIndex, objective, - otherObjective, + trimmedOtherObjective, context, emotion, style, - otherStyle, + trimmedOtherStyle, audience, structure, rhymes, @@ -257,11 +288,11 @@ const CreateLyricsWithAi = () => { config: { structure: chosenStructure }, selections: { objective, - otherObjective, + otherObjective: persistedOtherObjective, context, emotion, style, - otherStyle, + otherStyle: persistedOtherStyle, audience, structure, parsedStructure, @@ -433,11 +464,11 @@ const CreateLyricsWithAi = () => { config={lyricsConfig} selections={{ objective, - otherObjective, + otherObjective: persistedOtherObjective, context, emotion, style, - otherStyle, + otherStyle: persistedOtherStyle, audience, structure, parsedStructure, diff --git a/src/screens/Writing/CreateLyricsWithAi.web.js b/src/screens/Writing/CreateLyricsWithAi.web.js index 3b09656..14e4441 100644 --- a/src/screens/Writing/CreateLyricsWithAi.web.js +++ b/src/screens/Writing/CreateLyricsWithAi.web.js @@ -4,6 +4,7 @@ import { responsiveHeight } from "react-native-responsive-dimensions"; import { background } from "../../assets"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; +import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; import { goBack, navigate } from "../../navigation/NavigationService"; @@ -42,6 +43,18 @@ const CreateLyricsWithAi = () => { const [rhymes, setRhymes] = useState(null); const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain'] + const trimmedOtherObjective = + typeof otherObjective === "string" ? otherObjective.trim() : ""; + const shouldUseOtherObjective = objective === OTHER_OBJECTIVE_OPTION; + const persistedOtherObjective = shouldUseOtherObjective + ? trimmedOtherObjective + : ""; + + const trimmedOtherStyle = + typeof otherStyle === "string" ? otherStyle.trim() : ""; + const shouldUseOtherStyle = style === OTHER_STYLE_OPTION; + const persistedOtherStyle = shouldUseOtherStyle ? trimmedOtherStyle : ""; + // Pré-remplir les états depuis le projet sélectionné si disponibles React.useEffect(() => { if (!selectedProject) return; @@ -50,19 +63,28 @@ const CreateLyricsWithAi = () => { const rawOtherObjective = typeof sel.otherObjective === "string" ? sel.otherObjective : ""; - const hasSavedOtherObjective = rawOtherObjective.trim().length > 0; + const trimmedSavedOtherObjective = rawOtherObjective.trim(); + const hasSavedOtherObjective = trimmedSavedOtherObjective.length > 0; const savedObjective = typeof sel.objective === "string" && sel.objective.trim().length > 0 - ? sel.objective + ? sel.objective.trim() : null; if (!otherObjective && hasSavedOtherObjective) { setOtherObjective(rawOtherObjective); - if (objective !== null) { - setObjective(null); - } - } else if (!hasSavedOtherObjective && objective == null && savedObjective) { - setObjective(savedObjective); + } + + const shouldSelectOtherObjective = + hasSavedOtherObjective && + (!savedObjective || savedObjective === OTHER_OBJECTIVE_OPTION); + const resolvedSavedObjective = shouldSelectOtherObjective + ? OTHER_OBJECTIVE_OPTION + : savedObjective; + + if (resolvedSavedObjective && objective !== resolvedSavedObjective) { + setObjective(resolvedSavedObjective); + } else if (!resolvedSavedObjective && objective !== null) { + setObjective(null); } if (!context && typeof sel.context === "string") setContext(sel.context); @@ -84,19 +106,24 @@ const CreateLyricsWithAi = () => { const rawOtherStyle = typeof sel.otherStyle === "string" ? sel.otherStyle : ""; - const hasSavedOtherStyle = rawOtherStyle.trim().length > 0; + const trimmedSavedOtherStyle = rawOtherStyle.trim(); + const hasSavedOtherStyle = trimmedSavedOtherStyle.length > 0; const savedStyle = typeof sel.style === "string" && sel.style.trim().length > 0 - ? sel.style + ? sel.style.trim() : null; - if (!otherStyle && hasSavedOtherStyle) { setOtherStyle(rawOtherStyle); - if (style !== null) { - setStyle(null); - } - } else if (!hasSavedOtherStyle && style == null && savedStyle) { - setStyle(savedStyle); + } + const shouldSelectOtherStyle = + hasSavedOtherStyle && (!savedStyle || savedStyle === OTHER_STYLE_OPTION); + const resolvedSavedStyle = shouldSelectOtherStyle + ? OTHER_STYLE_OPTION + : savedStyle; + if (resolvedSavedStyle && style !== resolvedSavedStyle) { + setStyle(resolvedSavedStyle); + } else if (!resolvedSavedStyle && style !== null) { + setStyle(null); } if (!audience && typeof sel.audience === "string") setAudience(sel.audience); @@ -159,16 +186,21 @@ const CreateLyricsWithAi = () => { }, [hasLyrics]); const lyricsConfig = useMemo(() => { + const resolvedObjective = shouldUseOtherObjective + ? persistedOtherObjective || undefined + : objective || undefined; + const resolvedStyle = shouldUseOtherStyle + ? persistedOtherStyle || undefined + : style || undefined; + return { - objective: otherObjective?.trim() - ? otherObjective.trim() - : objective || undefined, + objective: resolvedObjective, context: context?.trim() ? context.trim() : undefined, emotion: emotion?.title && emotion?.description ? `${emotion.title} : ${emotion.description}` : undefined, - style: otherStyle?.trim() ? otherStyle.trim() : style || undefined, + style: resolvedStyle, audience: audience?.trim() ? audience.trim() : undefined, structure: (customStructure && @@ -179,12 +211,14 @@ const CreateLyricsWithAi = () => { rhymes: rhymes || undefined, }; }, [ + shouldUseOtherObjective, + persistedOtherObjective, objective, - otherObjective, + shouldUseOtherStyle, + persistedOtherStyle, context, emotion, style, - otherStyle, audience, parsedStructure, rhymes, @@ -196,9 +230,7 @@ const CreateLyricsWithAi = () => { case 0: { const hasObjective = typeof objective === "string" && objective.length > 0; - const hasOther = - typeof otherObjective === "string" && - otherObjective.trim().length > 0; + const hasOther = trimmedOtherObjective.length > 0; return !(hasObjective || hasOther); } case 1: { @@ -214,8 +246,7 @@ const CreateLyricsWithAi = () => { } case 3: { const hasStyle = typeof style === "string" && style.length > 0; - const hasOther = - typeof otherStyle === "string" && otherStyle.trim().length > 0; + const hasOther = trimmedOtherStyle.length > 0; return !(hasStyle || hasOther); } case 4: { @@ -238,11 +269,11 @@ const CreateLyricsWithAi = () => { }, [ selectedIndex, objective, - otherObjective, + trimmedOtherObjective, context, emotion, style, - otherStyle, + trimmedOtherStyle, audience, structure, rhymes, @@ -262,11 +293,11 @@ const CreateLyricsWithAi = () => { config: { structure: chosenStructure }, selections: { objective, - otherObjective, + otherObjective: persistedOtherObjective, context, emotion, style, - otherStyle, + otherStyle: persistedOtherStyle, audience, structure, parsedStructure, @@ -365,11 +396,11 @@ const CreateLyricsWithAi = () => { config={lyricsConfig} selections={{ objective, - otherObjective, + otherObjective: persistedOtherObjective, context, emotion, style, - otherStyle, + otherStyle: persistedOtherStyle, audience, structure, parsedStructure, diff --git a/src/screens/Writing/Goals.js b/src/screens/Writing/Goals.js index e2f2110..2d7f287 100644 --- a/src/screens/Writing/Goals.js +++ b/src/screens/Writing/Goals.js @@ -1,8 +1,8 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { StyleSheet, View } from "react-native"; import ItemContainer from "../../components/ItemContainer/ItemContainer"; import ListSelection from "../../components/ListSelection/ListSelection"; -import { GOALS } from "../../data/data"; +import { GOALS, OTHER_OBJECTIVE_OPTION } from "../../data/data"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import CreateLyricsHeader from "./components/CreateLyricsHeader"; @@ -19,9 +19,16 @@ const Goals = ({ const selected = selectedProp ?? internalSelected; const setSelected = setSelectedProp ?? setInternalSelected; + const goalsOptions = useMemo(() => GOALS ?? [], []); + const handleOtherObjectiveChange = (value) => { setOtherObjective?.(value); - if (value?.trim()?.length && selected != null) { + const trimmed = value?.trim() ?? ""; + if (trimmed.length) { + if (selected !== OTHER_OBJECTIVE_OPTION) { + setSelected(OTHER_OBJECTIVE_OPTION); + } + } else if (selected === OTHER_OBJECTIVE_OPTION) { setSelected(null); } }; @@ -34,7 +41,7 @@ const Goals = ({ SONG_STYLE ?? [], []); + const handleOtherStyleChange = (value) => { setOtherStyle?.(value); - if (value?.trim()?.length && selected != null) { + const trimmed = value?.trim() ?? ""; + if (trimmed.length) { + if (selected !== OTHER_STYLE_OPTION) { + setSelected(OTHER_STYLE_OPTION); + } + } else if (selected === OTHER_STYLE_OPTION) { setSelected(null); } }; @@ -36,7 +43,7 @@ const SongStyle = ({ />