first flow ok
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.itemContainer}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowText}>{item.label}</Text>
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}, []);
|
||||
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"
|
||||
/>
|
||||
<View style={styles.listContainer}>
|
||||
<Animated.ScrollView
|
||||
ref={scrollRef}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Sortable.Grid
|
||||
columns={1}
|
||||
data={items}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
rowGap={12}
|
||||
customHandle
|
||||
scrollableRef={scrollRef}
|
||||
onDragEnd={({ data: nextItems }) => {
|
||||
const nextOrder = nextItems.map((item) => item.value);
|
||||
setItems(buildItems(nextOrder));
|
||||
}}
|
||||
/>
|
||||
</Animated.ScrollView>
|
||||
<View style={styles.listContainer} ref={containerRef}>
|
||||
{items.map((item, index) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.itemContainer,
|
||||
activeItemId === item.id && styles.activeItem,
|
||||
]}
|
||||
onStartShouldSetResponder={() => true}
|
||||
onResponderGrant={(event) => handleGrant(index, event)}
|
||||
onResponderMove={handleMove}
|
||||
onResponderRelease={handleRelease}
|
||||
onResponderTerminate={handleRelease}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowText}>{item.label}</Text>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user