first flow ok
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user