first flow ok

This commit is contained in:
2025-10-01 16:14:31 +02:00
parent 9a2c0428e0
commit efc84ce2ab
2 changed files with 131 additions and 52 deletions
@@ -119,6 +119,13 @@ const CreateLyricsWithAi = () => {
} }
}, [selectedProject]); }, [selectedProject]);
React.useEffect(() => {
console.debug("[CreateLyricsWithAi.web] structure state", {
structure,
customStructure,
});
}, [structure, customStructure]);
const parsedStructure = useMemo(() => { const parsedStructure = useMemo(() => {
// Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain" // Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain"
try { try {
+124 -52
View File
@@ -1,14 +1,15 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React from "react";
import { Image, StyleSheet, View, Text } from "react-native"; import { Image, StyleSheet, Text, View } from "react-native";
import Animated, { useAnimatedRef } from "react-native-reanimated"; import { icons } from "../../assets";
import Sortable from "react-native-sortables";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; 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 VALID_SEGMENTS = ["couplet", "refrain"];
const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0);
const sanitizeStructure = (structure = []) => const sanitizeStructure = (structure = []) =>
structure structure
.map((segment) => `${segment}`.trim().toLowerCase()) .map((segment) => `${segment}`.trim().toLowerCase())
@@ -18,48 +19,118 @@ const buildItems = (structure = []) => {
const counters = { couplet: 0, refrain: 0 }; const counters = { couplet: 0, refrain: 0 };
return structure.map((segment, index) => { return structure.map((segment, index) => {
counters[segment] += 1; counters[segment] += 1;
const label = `${segment === "couplet" ? "Couplet" : "Refrain"} ${counters[segment]}`;
return { return {
id: `${segment}-${index}-${counters[segment]}`, id: `${segment}-${index}-${counters[segment]}`,
value: segment, value: segment,
label, label: `${segment === "couplet" ? "Couplet" : "Refrain"} ${
counters[segment]
}`,
}; };
}); });
}; };
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { const reorder = (list, from, to) => {
const sanitized = useMemo(() => sanitizeStructure(baseStructure), [baseStructure]); if (from === to) return list;
const [items, setItems] = useState(() => buildItems(sanitized)); const next = [...list];
const scrollRef = useAnimatedRef(); 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) => { setItems((prev) => {
if ( if (
prev.length === sanitized.length && prev.length === sanitized.length &&
prev.every((item, index) => item.value === sanitized[index]) prev.every((item, idx) => item.value === sanitized[idx])
) { ) {
return prev; return prev;
} }
return buildItems(sanitized); return buildItems(sanitized);
}); });
dragStateRef.current = { itemId: null, containerTop: 0 };
setActiveItemId(null);
}, [sanitized]); }, [sanitized]);
useEffect(() => { React.useEffect(() => {
console.debug("[CustomizeSongStructure.web] items", items);
onChange?.(items.map((item) => item.value)); onChange?.(items.map((item) => item.value));
}, [items, onChange]); }, [items, onChange]);
const renderItem = useCallback(({ item }) => { const handleGrant = (index, event) => {
return ( const item = items[index];
<View style={styles.itemContainer}> if (!item) return;
<View style={styles.row}> const rect = containerRef.current?.getBoundingClientRect();
<Text style={styles.rowText}>{item.label}</Text> const scrollY = getScrollY();
<Sortable.Handle> const nativeEvent = event.nativeEvent || {};
<Image source={icons.dragDots} style={styles.handleIcon} /> const fallbackY =
</Sortable.Handle> typeof nativeEvent.pageY === "number"
</View> ? nativeEvent.pageY
</View> : 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) { if (!items.length) {
return ( return (
@@ -78,26 +149,26 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
title="Personnalise la structure de ta chanson" title="Personnalise la structure de ta chanson"
subTitle="Glisse-dépose pour réorganiser" subTitle="Glisse-dépose pour réorganiser"
/> />
<View style={styles.listContainer}> <View style={styles.listContainer} ref={containerRef}>
<Animated.ScrollView {items.map((item, index) => (
ref={scrollRef} <View
contentContainerStyle={styles.scrollContent} key={item.id}
showsVerticalScrollIndicator={false} style={[
> styles.itemContainer,
<Sortable.Grid activeItemId === item.id && styles.activeItem,
columns={1} ]}
data={items} onStartShouldSetResponder={() => true}
keyExtractor={(item) => item.id} onResponderGrant={(event) => handleGrant(index, event)}
renderItem={renderItem} onResponderMove={handleMove}
rowGap={12} onResponderRelease={handleRelease}
customHandle onResponderTerminate={handleRelease}
scrollableRef={scrollRef} >
onDragEnd={({ data: nextItems }) => { <View style={styles.row}>
const nextOrder = nextItems.map((item) => item.value); <Text style={styles.rowText}>{item.label}</Text>
setItems(buildItems(nextOrder)); <Image source={icons.dragDots} style={styles.handleIcon} />
}} </View>
/> </View>
</Animated.ScrollView> ))}
</View> </View>
</View> </View>
); );
@@ -108,13 +179,10 @@ export default CustomizeSongStructure;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listContainer: { listContainer: {
flex: 1, flex: 1,
}, gap: 12,
scrollContent: {
paddingVertical: 4,
paddingBottom: 24,
}, },
itemContainer: { itemContainer: {
height: 54, height: ROW_HEIGHT,
borderRadius: 14, borderRadius: 14,
backgroundColor: Palette.glass, backgroundColor: Palette.glass,
shadowColor: "#00000040", shadowColor: "#00000040",
@@ -124,6 +192,10 @@ const styles = StyleSheet.create({
elevation: 5, elevation: 5,
overflow: "hidden", overflow: "hidden",
}, },
activeItem: {
borderWidth: 1,
borderColor: Palette.white,
},
row: { row: {
flex: 1, flex: 1,
paddingHorizontal: 16, paddingHorizontal: 16,