feat: 2 columns web for structure
This commit is contained in:
@@ -7,6 +7,7 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -28,6 +29,29 @@ import CreateLyricsHeader from "./components/CreateLyricsHeader";
|
|||||||
|
|
||||||
const ROW_HEIGHT = 54;
|
const ROW_HEIGHT = 54;
|
||||||
|
|
||||||
|
const PALETTE_SEGMENT_OVERRIDES = [
|
||||||
|
{
|
||||||
|
type: "pont",
|
||||||
|
description:
|
||||||
|
"Transition contrastée qui relance l’énergie avant d’entrer dans la dernière partie.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "interlude_melodique",
|
||||||
|
description:
|
||||||
|
"Respiration instrumentale qui met la mélodie à l’honneur sans paroles.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "final_apogee",
|
||||||
|
description:
|
||||||
|
"Clôture apothéose qui amplifie l’émotion pour un final impactant.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "fade_out",
|
||||||
|
description:
|
||||||
|
"Sortie progressive où le morceau disparaît doucement dans le mix.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0);
|
const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0);
|
||||||
|
|
||||||
const randomSuffix = () => Math.random().toString(36).slice(2, 8);
|
const randomSuffix = () => Math.random().toString(36).slice(2, 8);
|
||||||
@@ -62,11 +86,19 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
buildSortableItems(sanitizedBase)
|
buildSortableItems(sanitizedBase)
|
||||||
);
|
);
|
||||||
const [activeItemId, setActiveItemId] = useState(null);
|
const [activeItemId, setActiveItemId] = useState(null);
|
||||||
|
const [insertPreviewIndex, setInsertPreviewIndex] = useState(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
const sortableItemsRef = useRef(sortableItems);
|
||||||
const dragStateRef = useRef({
|
const dragStateRef = useRef({
|
||||||
itemId: null,
|
itemId: null,
|
||||||
|
paletteType: null,
|
||||||
|
pointerId: null,
|
||||||
containerTop: 0,
|
containerTop: 0,
|
||||||
|
containerRect: null,
|
||||||
|
targetIndex: null,
|
||||||
|
isOverContainer: false,
|
||||||
});
|
});
|
||||||
|
const globalListenersRef = useRef({ move: null, up: null });
|
||||||
|
|
||||||
const extractValues = useCallback(
|
const extractValues = useCallback(
|
||||||
(items) =>
|
(items) =>
|
||||||
@@ -84,6 +116,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
});
|
});
|
||||||
}, [sanitizedBase, extractValues]);
|
}, [sanitizedBase, extractValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sortableItemsRef.current = sortableItems;
|
||||||
|
}, [sortableItems]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange?.(extractValues(sortableItems));
|
onChange?.(extractValues(sortableItems));
|
||||||
}, [sortableItems, extractValues, onChange]);
|
}, [sortableItems, extractValues, onChange]);
|
||||||
@@ -103,11 +139,18 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
});
|
});
|
||||||
}, [sortableItems]);
|
}, [sortableItems]);
|
||||||
|
|
||||||
const optionalSegments = useMemo(
|
const paletteSegments = useMemo(
|
||||||
() =>
|
() =>
|
||||||
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
||||||
const meta = getSegmentMeta(type);
|
const meta = getSegmentMeta(type);
|
||||||
return { type, label: meta?.label || formatStructureLabel(type, 1) };
|
const override = PALETTE_SEGMENT_OVERRIDES.find(
|
||||||
|
(segment) => segment.type === type
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
label: meta?.label || formatStructureLabel(type, 1),
|
||||||
|
description: override?.description || "",
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -131,8 +174,8 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
[extractValues]
|
[extractValues]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOptionalSegmentPress = useCallback(
|
const addOptionalSegment = useCallback(
|
||||||
(type) => {
|
(type, insertIndex = null) => {
|
||||||
const meta = getSegmentMeta(type);
|
const meta = getSegmentMeta(type);
|
||||||
setSortableItems((prev) => {
|
setSortableItems((prev) => {
|
||||||
const currentValues = extractValues(prev);
|
const currentValues = extractValues(prev);
|
||||||
@@ -145,14 +188,16 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowsMultiple = meta?.allowMultiple !== false;
|
if (meta?.allowMultiple === false) {
|
||||||
if (!allowsMultiple && nextValues.includes(type)) {
|
nextValues = nextValues.filter((value) => value !== type);
|
||||||
const filtered = nextValues.filter((value) => value !== type);
|
|
||||||
const sanitized = sanitizeStructureList(filtered);
|
|
||||||
return buildSortableItems(sanitized);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextValues.push(type);
|
const targetIndex =
|
||||||
|
typeof insertIndex === "number"
|
||||||
|
? clamp(insertIndex, 0, nextValues.length)
|
||||||
|
: nextValues.length;
|
||||||
|
|
||||||
|
nextValues.splice(targetIndex, 0, type);
|
||||||
const sanitized = sanitizeStructureList(nextValues);
|
const sanitized = sanitizeStructureList(nextValues);
|
||||||
return buildSortableItems(sanitized);
|
return buildSortableItems(sanitized);
|
||||||
});
|
});
|
||||||
@@ -160,69 +205,302 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
[extractValues]
|
[extractValues]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGrant = useCallback((itemId, event) => {
|
const resetDragState = useCallback(() => {
|
||||||
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 = {
|
dragStateRef.current = {
|
||||||
itemId: null,
|
itemId: null,
|
||||||
|
paletteType: null,
|
||||||
|
pointerId: null,
|
||||||
containerTop: 0,
|
containerTop: 0,
|
||||||
|
containerRect: null,
|
||||||
|
targetIndex: null,
|
||||||
|
isOverContainer: false,
|
||||||
};
|
};
|
||||||
setActiveItemId(null);
|
}, []);
|
||||||
|
|
||||||
|
const cleanupPointerListeners = useCallback(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const { move, up } = globalListenersRef.current;
|
||||||
|
if (move) {
|
||||||
|
window.removeEventListener("pointermove", move);
|
||||||
|
}
|
||||||
|
if (up) {
|
||||||
|
window.removeEventListener("pointerup", up);
|
||||||
|
window.removeEventListener("pointercancel", up);
|
||||||
|
}
|
||||||
|
globalListenersRef.current = { move: null, up: null };
|
||||||
|
}
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.body.style.userSelect = "";
|
document.body.style.userSelect = "";
|
||||||
|
document.body.style.cursor = "";
|
||||||
}
|
}
|
||||||
}, [extractValues]);
|
resetDragState();
|
||||||
|
}, [resetDragState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
cleanupPointerListeners();
|
||||||
|
};
|
||||||
|
}, [cleanupPointerListeners]);
|
||||||
|
|
||||||
|
const handleGlobalPointerMove = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const dragState = dragStateRef.current;
|
||||||
|
if (!dragState.itemId && !dragState.paletteType) return;
|
||||||
|
if (
|
||||||
|
dragState.pointerId != null &&
|
||||||
|
event?.pointerId != null &&
|
||||||
|
dragState.pointerId !== event.pointerId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollY = getScrollY();
|
||||||
|
const absoluteY =
|
||||||
|
typeof event.pageY === "number"
|
||||||
|
? event.pageY
|
||||||
|
: typeof event.clientY === "number"
|
||||||
|
? event.clientY + scrollY
|
||||||
|
: scrollY;
|
||||||
|
const pointerY = absoluteY - (dragState.containerTop || 0);
|
||||||
|
|
||||||
|
if (dragState.itemId) {
|
||||||
|
setSortableItems((prev) => {
|
||||||
|
const currentIndex = prev.findIndex(
|
||||||
|
(it) => it.id === dragState.itemId
|
||||||
|
);
|
||||||
|
if (currentIndex === -1) return prev;
|
||||||
|
const maxIndex = Math.max(prev.length - 1, 0);
|
||||||
|
const targetIndex = clamp(
|
||||||
|
Math.floor(pointerY / ROW_HEIGHT),
|
||||||
|
0,
|
||||||
|
maxIndex
|
||||||
|
);
|
||||||
|
if (targetIndex === currentIndex) return prev;
|
||||||
|
return reorder(prev, currentIndex, targetIndex);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = dragState.containerRect;
|
||||||
|
const listLength = sortableItemsRef.current.length;
|
||||||
|
const isOver =
|
||||||
|
rect == null
|
||||||
|
? true
|
||||||
|
: event.clientX >= rect.left &&
|
||||||
|
event.clientX <= rect.right &&
|
||||||
|
event.clientY >= rect.top &&
|
||||||
|
event.clientY <= rect.bottom;
|
||||||
|
dragStateRef.current.isOverContainer = isOver;
|
||||||
|
|
||||||
|
if (!isOver) {
|
||||||
|
if (insertPreviewIndex !== null) {
|
||||||
|
setInsertPreviewIndex(null);
|
||||||
|
}
|
||||||
|
dragStateRef.current.targetIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerHeight = rect?.height ?? listLength * ROW_HEIGHT;
|
||||||
|
let targetIndex = Math.floor(pointerY / ROW_HEIGHT);
|
||||||
|
if (pointerY < 0) targetIndex = 0;
|
||||||
|
if (pointerY > containerHeight) targetIndex = listLength;
|
||||||
|
targetIndex = clamp(targetIndex, 0, listLength);
|
||||||
|
|
||||||
|
if (dragStateRef.current.targetIndex !== targetIndex) {
|
||||||
|
dragStateRef.current.targetIndex = targetIndex;
|
||||||
|
setInsertPreviewIndex(targetIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[insertPreviewIndex, setSortableItems]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGlobalPointerUp = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const dragState = dragStateRef.current;
|
||||||
|
if (!dragState.itemId && !dragState.paletteType) return;
|
||||||
|
if (
|
||||||
|
dragState.pointerId != null &&
|
||||||
|
event?.pointerId != null &&
|
||||||
|
dragState.pointerId !== event.pointerId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragState.itemId) {
|
||||||
|
setSortableItems((prev) =>
|
||||||
|
buildSortableItems(extractValues(prev))
|
||||||
|
);
|
||||||
|
} else if (dragState.paletteType) {
|
||||||
|
let isOver = dragState.isOverContainer;
|
||||||
|
let targetIndex = dragState.targetIndex;
|
||||||
|
const rect = dragState.containerRect;
|
||||||
|
|
||||||
|
if (rect && (!isOver || typeof targetIndex !== "number")) {
|
||||||
|
const scrollY = getScrollY();
|
||||||
|
const absoluteY =
|
||||||
|
typeof event.pageY === "number"
|
||||||
|
? event.pageY
|
||||||
|
: typeof event.clientY === "number"
|
||||||
|
? event.clientY + scrollY
|
||||||
|
: scrollY;
|
||||||
|
const pointerY = absoluteY - (dragState.containerTop || 0);
|
||||||
|
const within =
|
||||||
|
event.clientX >= rect.left &&
|
||||||
|
event.clientX <= rect.right &&
|
||||||
|
event.clientY >= rect.top &&
|
||||||
|
event.clientY <= rect.bottom;
|
||||||
|
|
||||||
|
if (within) {
|
||||||
|
isOver = true;
|
||||||
|
const listLength = sortableItemsRef.current.length;
|
||||||
|
const containerHeight = rect?.height ?? listLength * ROW_HEIGHT;
|
||||||
|
let computedIndex = Math.floor(pointerY / ROW_HEIGHT);
|
||||||
|
if (pointerY < 0) computedIndex = 0;
|
||||||
|
if (pointerY > containerHeight) computedIndex = listLength;
|
||||||
|
targetIndex = clamp(computedIndex, 0, listLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOver && typeof targetIndex === "number") {
|
||||||
|
addOptionalSegment(dragState.paletteType, targetIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveItemId(null);
|
||||||
|
setInsertPreviewIndex(null);
|
||||||
|
cleanupPointerListeners();
|
||||||
|
},
|
||||||
|
[addOptionalSegment, cleanupPointerListeners, extractValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const attachGlobalListeners = useCallback(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
if (globalListenersRef.current.move) return;
|
||||||
|
|
||||||
|
const moveListener = (event) => handleGlobalPointerMove(event);
|
||||||
|
const upListener = (event) => handleGlobalPointerUp(event);
|
||||||
|
|
||||||
|
window.addEventListener("pointermove", moveListener);
|
||||||
|
window.addEventListener("pointerup", upListener);
|
||||||
|
window.addEventListener("pointercancel", upListener);
|
||||||
|
|
||||||
|
globalListenersRef.current = {
|
||||||
|
move: moveListener,
|
||||||
|
up: upListener,
|
||||||
|
};
|
||||||
|
}, [handleGlobalPointerMove, handleGlobalPointerUp]);
|
||||||
|
|
||||||
|
const startItemDrag = useCallback(
|
||||||
|
(itemId, event) => {
|
||||||
|
const nativeEvent = event?.nativeEvent || {};
|
||||||
|
if (typeof nativeEvent.button === "number" && nativeEvent.button !== 0)
|
||||||
|
return;
|
||||||
|
event?.preventDefault?.();
|
||||||
|
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
|
const scrollY = getScrollY();
|
||||||
|
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,
|
||||||
|
paletteType: null,
|
||||||
|
pointerId:
|
||||||
|
typeof nativeEvent.pointerId === "number"
|
||||||
|
? nativeEvent.pointerId
|
||||||
|
: null,
|
||||||
|
containerTop,
|
||||||
|
containerRect: rect
|
||||||
|
? {
|
||||||
|
top: rect.top,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
left: rect.left,
|
||||||
|
right: rect.right,
|
||||||
|
height: rect.height,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
targetIndex: null,
|
||||||
|
isOverContainer: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
setActiveItemId(itemId);
|
||||||
|
setInsertPreviewIndex(null);
|
||||||
|
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
|
}
|
||||||
|
|
||||||
|
attachGlobalListeners();
|
||||||
|
},
|
||||||
|
[attachGlobalListeners]
|
||||||
|
);
|
||||||
|
|
||||||
|
const startPaletteDrag = useCallback(
|
||||||
|
(type, event) => {
|
||||||
|
const nativeEvent = event?.nativeEvent || {};
|
||||||
|
if (typeof nativeEvent.button === "number" && nativeEvent.button !== 0)
|
||||||
|
return;
|
||||||
|
event?.preventDefault?.();
|
||||||
|
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
|
const scrollY = getScrollY();
|
||||||
|
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: null,
|
||||||
|
paletteType: type,
|
||||||
|
pointerId:
|
||||||
|
typeof nativeEvent.pointerId === "number"
|
||||||
|
? nativeEvent.pointerId
|
||||||
|
: null,
|
||||||
|
containerTop,
|
||||||
|
containerRect: rect
|
||||||
|
? {
|
||||||
|
top: rect.top,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
left: rect.left,
|
||||||
|
right: rect.right,
|
||||||
|
height: rect.height,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
targetIndex: null,
|
||||||
|
isOverContainer: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
setActiveItemId(`palette-${type}`);
|
||||||
|
setInsertPreviewIndex(null);
|
||||||
|
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
document.body.style.cursor = "grabbing";
|
||||||
|
}
|
||||||
|
|
||||||
|
attachGlobalListeners();
|
||||||
|
},
|
||||||
|
[attachGlobalListeners]
|
||||||
|
);
|
||||||
|
|
||||||
const StructureRow = ({ item }) => {
|
const StructureRow = ({ item }) => {
|
||||||
const baseType = item?.type || item?.value;
|
const baseType = item?.type || item?.value;
|
||||||
const meta = getSegmentMeta(baseType);
|
const meta = getSegmentMeta(baseType);
|
||||||
const canRemove = meta?.optional === true;
|
const canRemove = meta?.optional === true;
|
||||||
|
const isIntroduction =
|
||||||
|
baseType === "short_intro" ||
|
||||||
|
baseType === "long_intro" ||
|
||||||
|
baseType === "intro" ||
|
||||||
|
baseType === "introduction";
|
||||||
const containerStyle = StyleSheet.flatten([
|
const containerStyle = StyleSheet.flatten([
|
||||||
styles.rowContainer,
|
styles.rowContainer,
|
||||||
activeItemId === item.id ? styles.activeItem : null,
|
activeItemId === item.id ? styles.activeItem : null,
|
||||||
@@ -252,16 +530,14 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
<Image source={icons.close} style={styles.removeIcon} />
|
<Image source={icons.close} style={styles.removeIcon} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<View
|
{!isIntroduction && (
|
||||||
style={styles.handleWrapper}
|
<View
|
||||||
onStartShouldSetResponder={() => true}
|
style={styles.handleWrapper}
|
||||||
onResponderGrant={(event) => handleGrant(item.id, event)}
|
onPointerDown={(event) => startItemDrag(item.id, event)}
|
||||||
onResponderMove={handleMove}
|
>
|
||||||
onResponderRelease={handleRelease}
|
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||||
onResponderTerminate={handleRelease}
|
</View>
|
||||||
>
|
)}
|
||||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</CreateLyricsHeader>
|
</CreateLyricsHeader>
|
||||||
@@ -269,74 +545,122 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
|
<View style={styles.container}>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
title="Personnalise la structure de ta chanson"
|
title="Personnalise la structure de ta chanson"
|
||||||
subTitle="Réorganise par glisser-déposer"
|
subTitle="Réorganise par glisser-déposer"
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1 }}>
|
<ScrollView
|
||||||
<ScrollView
|
contentContainerStyle={styles.scrollContent}
|
||||||
contentContainerStyle={styles.scrollContent}
|
showsVerticalScrollIndicator={false}
|
||||||
showsVerticalScrollIndicator={false}
|
>
|
||||||
>
|
<View style={styles.columns}>
|
||||||
<View style={styles.sectionTag}>
|
<View style={styles.column}>
|
||||||
<Text style={styles.sectionTagText}>Structure actuelle</Text>
|
<View style={styles.sectionTag}>
|
||||||
</View>
|
<Text style={styles.sectionTagText}>Structure actuelle</Text>
|
||||||
<ItemContainer
|
|
||||||
height="auto"
|
|
||||||
disableKeyboardHeight
|
|
||||||
style={styles.blurSection}
|
|
||||||
>
|
|
||||||
<View style={styles.listContainer} ref={containerRef}>
|
|
||||||
{labeledItems.map((item) => (
|
|
||||||
<StructureRow key={item.id} item={item} />
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
</ItemContainer>
|
<ItemContainer
|
||||||
<View style={[styles.sectionTag, styles.addSectionTag]}>
|
height="auto"
|
||||||
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
disableKeyboardHeight
|
||||||
|
style={styles.blurSection}
|
||||||
|
>
|
||||||
|
<View style={styles.listContainer} ref={containerRef}>
|
||||||
|
{labeledItems.map((item, index) => (
|
||||||
|
<React.Fragment key={item.id}>
|
||||||
|
{insertPreviewIndex === index && (
|
||||||
|
<View style={styles.dropPreview} />
|
||||||
|
)}
|
||||||
|
<StructureRow item={item} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
{insertPreviewIndex != null &&
|
||||||
|
insertPreviewIndex >= labeledItems.length && (
|
||||||
|
<View style={styles.dropPreview} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ItemContainer>
|
||||||
</View>
|
</View>
|
||||||
<ItemContainer
|
<View style={[styles.column, styles.paletteColumn]}>
|
||||||
height="auto"
|
<View style={[styles.sectionTag, styles.addSectionTag]}>
|
||||||
disableKeyboardHeight
|
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
||||||
style={styles.blurSection}
|
</View>
|
||||||
>
|
<ItemContainer
|
||||||
<View style={styles.optionsContainer}>
|
height="auto"
|
||||||
{optionalSegments.map((segment) => {
|
disableKeyboardHeight
|
||||||
const count = segmentCounts[segment.type] || 0;
|
style={styles.blurSection}
|
||||||
const selected = count > 0;
|
>
|
||||||
const blurStyle = StyleSheet.flatten([
|
<View style={styles.paletteList}>
|
||||||
styles.itemBlur,
|
{paletteSegments.map((segment) => {
|
||||||
selected ? styles.selectedBlur : null,
|
const count = segmentCounts[segment.type] || 0;
|
||||||
]);
|
const isActive = activeItemId === `palette-${segment.type}`;
|
||||||
const badgeStyle = StyleSheet.flatten([
|
const blurStyle = StyleSheet.flatten([
|
||||||
styles.countBadge,
|
styles.itemBlur,
|
||||||
selected ? styles.countBadgeActive : null,
|
styles.paletteItemBlur,
|
||||||
]);
|
isActive ? styles.activeBlur : null,
|
||||||
|
]);
|
||||||
|
const badgeStyle = StyleSheet.flatten([
|
||||||
|
styles.countBadge,
|
||||||
|
count > 0 ? styles.countBadgeActive : null,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateLyricsHeader
|
<View
|
||||||
key={segment.type}
|
key={segment.type}
|
||||||
tint="dark"
|
style={styles.paletteCardWrapper}
|
||||||
intensity={40}
|
onPointerDown={(event) =>
|
||||||
showBorder={false}
|
startPaletteDrag(segment.type, event)
|
||||||
onPress={() => handleOptionalSegmentPress(segment.type)}
|
}
|
||||||
containerStyle={styles.rowContainer}
|
>
|
||||||
blurViewStyle={blurStyle}
|
<CreateLyricsHeader
|
||||||
>
|
tint="dark"
|
||||||
<View style={styles.row}>
|
intensity={40}
|
||||||
<Text style={styles.rowText}>{segment.label}</Text>
|
showBorder={false}
|
||||||
<View style={badgeStyle}>
|
containerStyle={styles.rowContainer}
|
||||||
<Text style={styles.countBadgeText}>{count}</Text>
|
blurViewStyle={blurStyle}
|
||||||
</View>
|
>
|
||||||
|
<View style={styles.paletteRow}>
|
||||||
|
<View style={styles.paletteTextWrapper}>
|
||||||
|
<Text style={styles.rowText}>{segment.label}</Text>
|
||||||
|
{segment.description ? (
|
||||||
|
<Text style={styles.paletteDescription}>
|
||||||
|
{segment.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<View style={styles.paletteActions}>
|
||||||
|
<Pressable
|
||||||
|
onPointerDown={(ev) => ev?.stopPropagation?.()}
|
||||||
|
onPressIn={(ev) => ev?.stopPropagation?.()}
|
||||||
|
onPress={(ev) => {
|
||||||
|
ev?.stopPropagation?.();
|
||||||
|
addOptionalSegment(segment.type);
|
||||||
|
setInsertPreviewIndex(null);
|
||||||
|
setActiveItemId(null);
|
||||||
|
}}
|
||||||
|
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||||
|
style={({ pressed }) =>
|
||||||
|
StyleSheet.flatten([
|
||||||
|
styles.addButton,
|
||||||
|
pressed ? styles.addButtonPressed : null,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image source={icons.add} style={styles.addIcon} />
|
||||||
|
</Pressable>
|
||||||
|
<View style={badgeStyle}>
|
||||||
|
<Text style={styles.countBadgeText}>{count}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</CreateLyricsHeader>
|
||||||
</View>
|
</View>
|
||||||
</CreateLyricsHeader>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</View>
|
||||||
</View>
|
</ItemContainer>
|
||||||
</ItemContainer>
|
</View>
|
||||||
</ScrollView>
|
</View>
|
||||||
</View>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -344,9 +668,30 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
export default CustomizeSongStructure;
|
export default CustomizeSongStructure;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 10,
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
gap: 12,
|
gap: 24,
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 24,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexBasis: 0,
|
||||||
|
minWidth: 320,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
paletteColumn: {
|
||||||
|
flexBasis: 360,
|
||||||
|
maxWidth: 420,
|
||||||
},
|
},
|
||||||
sectionTag: {
|
sectionTag: {
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
@@ -354,10 +699,9 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
backgroundColor: "rgba(255,255,255,0.12)",
|
backgroundColor: "rgba(255,255,255,0.12)",
|
||||||
marginTop: 12,
|
|
||||||
},
|
},
|
||||||
addSectionTag: {
|
addSectionTag: {
|
||||||
marginTop: 24,
|
marginTop: 0,
|
||||||
},
|
},
|
||||||
sectionTagText: {
|
sectionTagText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -366,12 +710,19 @@ const styles = StyleSheet.create({
|
|||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
},
|
},
|
||||||
|
blurSection: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
listContainer: {
|
listContainer: {
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
blurSection: {
|
dropPreview: {
|
||||||
marginTop: 12,
|
height: ROW_HEIGHT,
|
||||||
width: "100%",
|
borderRadius: 14,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "dashed",
|
||||||
|
borderColor: Palette.transparentWhite,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
},
|
},
|
||||||
rowContainer: {
|
rowContainer: {
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
@@ -387,10 +738,13 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
activeBlur: {
|
paletteItemBlur: {
|
||||||
backgroundColor: Palette.transparentWhite,
|
alignItems: "stretch",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
paddingVertical: 16,
|
||||||
},
|
},
|
||||||
selectedBlur: {
|
activeBlur: {
|
||||||
backgroundColor: Palette.transparentWhite,
|
backgroundColor: Palette.transparentWhite,
|
||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
@@ -429,9 +783,46 @@ const styles = StyleSheet.create({
|
|||||||
tintColor: Palette.white,
|
tintColor: Palette.white,
|
||||||
cursor: "grab",
|
cursor: "grab",
|
||||||
},
|
},
|
||||||
optionsContainer: {
|
paletteList: {
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
|
paletteCardWrapper: {
|
||||||
|
cursor: "grab",
|
||||||
|
},
|
||||||
|
paletteRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
paletteTextWrapper: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
paletteDescription: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
color: "rgba(255,255,255,0.75)",
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
},
|
||||||
|
paletteActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
|
},
|
||||||
|
addButtonPressed: {
|
||||||
|
backgroundColor: "rgba(255,255,255,0.18)",
|
||||||
|
},
|
||||||
|
addIcon: {
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
tintColor: Palette.white,
|
||||||
|
},
|
||||||
countBadge: {
|
countBadge: {
|
||||||
minWidth: 28,
|
minWidth: 28,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
|
|||||||
Reference in New Issue
Block a user