diff --git a/src/screens/Writing/CreateLyricsWithAi.web.js b/src/screens/Writing/CreateLyricsWithAi.web.js index fb2c89f..3b09656 100644 --- a/src/screens/Writing/CreateLyricsWithAi.web.js +++ b/src/screens/Writing/CreateLyricsWithAi.web.js @@ -119,6 +119,13 @@ const CreateLyricsWithAi = () => { } }, [selectedProject]); + React.useEffect(() => { + console.debug("[CreateLyricsWithAi.web] structure state", { + structure, + customStructure, + }); + }, [structure, customStructure]); + const parsedStructure = useMemo(() => { // Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain" try { diff --git a/src/screens/Writing/CustomizeSongStructure.web.js b/src/screens/Writing/CustomizeSongStructure.web.js index 79b0975..b70df4f 100644 --- a/src/screens/Writing/CustomizeSongStructure.web.js +++ b/src/screens/Writing/CustomizeSongStructure.web.js @@ -1,14 +1,15 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Image, StyleSheet, View, Text } from "react-native"; -import Animated, { useAnimatedRef } from "react-native-reanimated"; -import Sortable from "react-native-sortables"; -import CreateLyricsHeader from "./components/CreateLyricsHeader"; +import React from "react"; +import { Image, StyleSheet, Text, View } from "react-native"; +import { icons } from "../../assets"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; -import { icons } from "../../assets"; +import CreateLyricsHeader from "./components/CreateLyricsHeader"; +const ROW_HEIGHT = 54; const VALID_SEGMENTS = ["couplet", "refrain"]; +const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0); + const sanitizeStructure = (structure = []) => structure .map((segment) => `${segment}`.trim().toLowerCase()) @@ -18,48 +19,118 @@ const buildItems = (structure = []) => { const counters = { couplet: 0, refrain: 0 }; return structure.map((segment, index) => { counters[segment] += 1; - const label = `${segment === "couplet" ? "Couplet" : "Refrain"} ${counters[segment]}`; return { id: `${segment}-${index}-${counters[segment]}`, value: segment, - label, + label: `${segment === "couplet" ? "Couplet" : "Refrain"} ${ + counters[segment] + }`, }; }); }; -const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { - const sanitized = useMemo(() => sanitizeStructure(baseStructure), [baseStructure]); - const [items, setItems] = useState(() => buildItems(sanitized)); - const scrollRef = useAnimatedRef(); +const reorder = (list, from, to) => { + if (from === to) return list; + const next = [...list]; + const [moved] = next.splice(from, 1); + next.splice(to, 0, moved); + return next; +}; - useEffect(() => { +const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); + +const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { + const sanitized = React.useMemo( + () => sanitizeStructure(baseStructure), + [baseStructure] + ); + const [items, setItems] = React.useState(() => buildItems(sanitized)); + const [activeItemId, setActiveItemId] = React.useState(null); + const containerRef = React.useRef(null); + const dragStateRef = React.useRef({ + itemId: null, + containerTop: 0, + }); + + React.useEffect(() => { + console.debug("[CustomizeSongStructure.web] sanitized", sanitized); setItems((prev) => { if ( prev.length === sanitized.length && - prev.every((item, index) => item.value === sanitized[index]) + prev.every((item, idx) => item.value === sanitized[idx]) ) { return prev; } return buildItems(sanitized); }); + dragStateRef.current = { itemId: null, containerTop: 0 }; + setActiveItemId(null); }, [sanitized]); - useEffect(() => { + React.useEffect(() => { + console.debug("[CustomizeSongStructure.web] items", items); onChange?.(items.map((item) => item.value)); }, [items, onChange]); - const renderItem = useCallback(({ item }) => { - return ( - - - {item.label} - - - - - - ); - }, []); + const handleGrant = (index, event) => { + const item = items[index]; + if (!item) return; + const rect = containerRef.current?.getBoundingClientRect(); + const scrollY = getScrollY(); + const nativeEvent = event.nativeEvent || {}; + const fallbackY = + typeof nativeEvent.pageY === "number" + ? nativeEvent.pageY + : typeof nativeEvent.clientY === "number" + ? nativeEvent.clientY + scrollY + : scrollY; + const containerTop = rect ? rect.top + scrollY : fallbackY; + dragStateRef.current = { + itemId: item.id, + containerTop, + }; + setActiveItemId(item.id); + if (typeof document !== "undefined") { + document.body.style.userSelect = "none"; + } + }; + + const handleMove = (event) => { + const { itemId, containerTop } = dragStateRef.current; + if (!itemId) return; + const nativeEvent = event.nativeEvent || {}; + const scrollY = getScrollY(); + const absoluteY = + typeof nativeEvent.pageY === "number" + ? nativeEvent.pageY + : typeof nativeEvent.clientY === "number" + ? nativeEvent.clientY + scrollY + : scrollY; + const pointerY = absoluteY - containerTop; + + setItems((prev) => { + const currentIndex = prev.findIndex((it) => it.id === itemId); + if (currentIndex === -1) return prev; + const targetIndex = clamp( + Math.floor(pointerY / ROW_HEIGHT), + 0, + Math.max(prev.length - 1, 0) + ); + if (targetIndex === currentIndex) return prev; + return reorder(prev, currentIndex, targetIndex); + }); + }; + + const handleRelease = () => { + dragStateRef.current = { + itemId: null, + containerTop: 0, + }; + setActiveItemId(null); + if (typeof document !== "undefined") { + document.body.style.userSelect = ""; + } + }; if (!items.length) { return ( @@ -78,26 +149,26 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { title="Personnalise la structure de ta chanson" subTitle="Glisse-dépose pour réorganiser" /> - - - item.id} - renderItem={renderItem} - rowGap={12} - customHandle - scrollableRef={scrollRef} - onDragEnd={({ data: nextItems }) => { - const nextOrder = nextItems.map((item) => item.value); - setItems(buildItems(nextOrder)); - }} - /> - + + {items.map((item, index) => ( + true} + onResponderGrant={(event) => handleGrant(index, event)} + onResponderMove={handleMove} + onResponderRelease={handleRelease} + onResponderTerminate={handleRelease} + > + + {item.label} + + + + ))} ); @@ -108,13 +179,10 @@ export default CustomizeSongStructure; const styles = StyleSheet.create({ listContainer: { flex: 1, - }, - scrollContent: { - paddingVertical: 4, - paddingBottom: 24, + gap: 12, }, itemContainer: { - height: 54, + height: ROW_HEIGHT, borderRadius: 14, backgroundColor: Palette.glass, shadowColor: "#00000040", @@ -124,6 +192,10 @@ const styles = StyleSheet.create({ elevation: 5, overflow: "hidden", }, + activeItem: { + borderWidth: 1, + borderColor: Palette.white, + }, row: { flex: 1, paddingHorizontal: 16,