import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Image, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { icons } from "../../assets"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { arraysAreSame, formatStructureLabel, getSegmentMeta, OPTIONAL_STRUCTURE_SEGMENTS, sanitizeStructureList, } from "../../utils/songStructure"; import CreateLyricsHeader from "./components/CreateLyricsHeader"; const ROW_HEIGHT = 54; const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0); const randomSuffix = () => Math.random().toString(36).slice(2, 8); const createSortableItem = (type, index = 0) => ({ id: `${type}-${index}-${randomSuffix()}`, type, value: type, }); const buildSortableItems = (values = []) => sanitizeStructureList(values).map((type, index) => createSortableItem(type, index) ); 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; }; const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { const sanitizedBase = useMemo( () => sanitizeStructureList(baseStructure), [baseStructure] ); const [sortableItems, setSortableItems] = useState(() => buildSortableItems(sanitizedBase) ); const [activeItemId, setActiveItemId] = useState(null); const containerRef = useRef(null); const dragStateRef = useRef({ itemId: null, containerTop: 0, }); const extractValues = useCallback( (items) => (items || []).map((item) => item?.type || item?.value).filter(Boolean), [] ); useEffect(() => { setSortableItems((prev) => { const currentValues = extractValues(prev); if (arraysAreSame(currentValues, sanitizedBase)) { return prev; } return buildSortableItems(sanitizedBase); }); }, [sanitizedBase, extractValues]); useEffect(() => { onChange?.(extractValues(sortableItems)); }, [sortableItems, extractValues, onChange]); const labeledItems = useMemo(() => { const counts = {}; return sortableItems.map((item) => { const baseType = item.type || item.value; const nextCount = (counts[baseType] || 0) + 1; counts[baseType] = nextCount; return { ...item, type: baseType, value: baseType, label: formatStructureLabel(baseType, nextCount), }; }); }, [sortableItems]); const optionalSegments = useMemo( () => OPTIONAL_STRUCTURE_SEGMENTS.map((type) => { const meta = getSegmentMeta(type); return { type, label: meta?.label || formatStructureLabel(type, 1) }; }), [] ); const segmentCounts = useMemo(() => { const counts = {}; extractValues(sortableItems).forEach((type) => { counts[type] = (counts[type] || 0) + 1; }); return counts; }, [sortableItems, extractValues]); const handleRemoveItem = useCallback( (itemId) => { setSortableItems((prev) => { const remaining = prev.filter((item) => item.id !== itemId); const sanitized = sanitizeStructureList(extractValues(remaining)); return buildSortableItems(sanitized); }); }, [extractValues] ); const handleOptionalSegmentPress = useCallback( (type) => { const meta = getSegmentMeta(type); setSortableItems((prev) => { const currentValues = extractValues(prev); let nextValues = [...currentValues]; if (meta?.exclusiveGroup) { nextValues = nextValues.filter((value) => { const itemMeta = getSegmentMeta(value); return itemMeta?.exclusiveGroup !== meta.exclusiveGroup; }); } const allowsMultiple = meta?.allowMultiple !== false; if (!allowsMultiple && nextValues.includes(type)) { const filtered = nextValues.filter((value) => value !== type); const sanitized = sanitizeStructureList(filtered); return buildSortableItems(sanitized); } nextValues.push(type); const sanitized = sanitizeStructureList(nextValues); return buildSortableItems(sanitized); }); }, [extractValues] ); const handleGrant = useCallback((itemId, event) => { 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, containerTop, }; setActiveItemId(itemId); if (typeof document !== "undefined") { document.body.style.userSelect = "none"; } }, []); const handleMove = useCallback((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; setSortableItems((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 = useCallback(() => { setSortableItems((prev) => buildSortableItems(extractValues(prev))); dragStateRef.current = { itemId: null, containerTop: 0, }; setActiveItemId(null); if (typeof document !== "undefined") { document.body.style.userSelect = ""; } }, [extractValues]); const StructureRow = ({ item }) => { const baseType = item?.type || item?.value; const meta = getSegmentMeta(baseType); const canRemove = meta?.optional === true; return ( {item.label} {canRemove && ( handleRemoveItem(item.id)} style={styles.removeButton} hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }} > )} true} onResponderGrant={(event) => handleGrant(item.id, event)} onResponderMove={handleMove} onResponderRelease={handleRelease} onResponderTerminate={handleRelease} > ); }; return ( Structure actuelle {labeledItems.map((item) => ( ))} Éléments à ajouter {optionalSegments.map((segment) => { const count = segmentCounts[segment.type] || 0; const selected = count > 0; return ( handleOptionalSegmentPress(segment.type)} style={[styles.optionItem, selected && styles.selectedItem]} > {segment.label} {count} ); })} ); }; export default CustomizeSongStructure; const styles = StyleSheet.create({ scrollContent: { paddingBottom: 24, gap: 12, }, sectionTag: { alignSelf: "flex-start", paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, backgroundColor: "rgba(255,255,255,0.12)", marginTop: 12, }, addSectionTag: { marginTop: 24, }, sectionTagText: { fontSize: 12, color: Palette.white, fontFamily: FONT_FAMILY.InterSemiBold, letterSpacing: 1, textTransform: "uppercase", }, listContainer: { gap: 12, }, itemContainer: { height: ROW_HEIGHT, borderRadius: 14, backgroundColor: Palette.glass, shadowColor: "#00000040", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, overflow: "hidden", }, activeItem: { borderWidth: 1, borderColor: Palette.white, }, row: { flex: 1, paddingHorizontal: 16, flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, rowText: { fontSize: 16, color: Palette.white, fontFamily: FONT_FAMILY.InterRegular, }, rowActions: { flexDirection: "row", alignItems: "center", }, removeButton: { padding: 6, borderRadius: 999, backgroundColor: "rgba(255,255,255,0.08)", marginRight: 8, }, removeIcon: { width: 16, height: 16, tintColor: Palette.white, }, handleWrapper: { padding: 6, borderRadius: 999, }, handleIcon: { width: 22, height: 22, tintColor: Palette.white, cursor: "grab", }, optionsContainer: { gap: 12, marginTop: 12, }, optionItem: { height: ROW_HEIGHT, borderRadius: 14, backgroundColor: Palette.glass, shadowColor: "#00000040", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, overflow: "hidden", paddingHorizontal: 16, flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, selectedItem: { borderWidth: 1, borderColor: Palette.white, }, countBadge: { minWidth: 28, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 999, backgroundColor: "rgba(255,255,255,0.08)", alignItems: "center", justifyContent: "center", }, countBadgeActive: { backgroundColor: Palette.transparentWhite, }, countBadgeText: { fontSize: 14, color: Palette.white, fontFamily: FONT_FAMILY.InterMedium, }, });