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,