feat: fix drag and drop
This commit is contained in:
@@ -21,6 +21,7 @@ import { gutters, size } from "../../styles/Style";
|
|||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
|
import CreditAmount from "../../components/CreditAmount";
|
||||||
|
|
||||||
const MUSIC_GENERATION_COIN_COST = 8;
|
const MUSIC_GENERATION_COIN_COST = 8;
|
||||||
const SONG_OPTIONS_PER_GENERATION = 2;
|
const SONG_OPTIONS_PER_GENERATION = 2;
|
||||||
@@ -65,7 +66,7 @@ const SongReady = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("SongReady pause error", error?.message);
|
console.log("SongReady pause error", error?.message);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -93,7 +94,7 @@ const SongReady = () => {
|
|||||||
console.log("SongReady play error", error?.message);
|
console.log("SongReady play error", error?.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pauseAllExcept],
|
[pauseAllExcept]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync URLs from provider's selectedProject
|
// Sync URLs from provider's selectedProject
|
||||||
@@ -110,8 +111,6 @@ const SongReady = () => {
|
|||||||
});
|
});
|
||||||
}, [selectedProject?.musicUrls]);
|
}, [selectedProject?.musicUrls]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const validateSelection = async () => {
|
const validateSelection = async () => {
|
||||||
try {
|
try {
|
||||||
const url = musicUrls[selectedIndex];
|
const url = musicUrls[selectedIndex];
|
||||||
@@ -150,7 +149,7 @@ const SongReady = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
pauseAllPlayers();
|
pauseAllPlayers();
|
||||||
};
|
};
|
||||||
}, [pauseAllPlayers]),
|
}, [pauseAllPlayers])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -177,7 +176,7 @@ const SongReady = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ cancelable: false },
|
{ cancelable: false }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -208,7 +207,7 @@ const SongReady = () => {
|
|||||||
onPress: () => handleConfirmRegenerate(),
|
onPress: () => handleConfirmRegenerate(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ cancelable: true },
|
{ cancelable: true }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,7 +233,7 @@ const SongReady = () => {
|
|||||||
marginBottom: responsiveHeight(2),
|
marginBottom: responsiveHeight(2),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={{ gap: 16 }}>
|
<View style={{ gap: 16, marginTop: isWeb ? gutters * 3 : 0 }}>
|
||||||
{musicUrls.map((url, idx) => (
|
{musicUrls.map((url, idx) => (
|
||||||
<SongOptionCard
|
<SongOptionCard
|
||||||
key={`${url || "song"}-${idx}`}
|
key={`${url || "song"}-${idx}`}
|
||||||
@@ -341,11 +340,11 @@ const SongOptionCard = ({
|
|||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
const durationMs = Math.max(
|
const durationMs = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.round((Number(player.duration) || 0) * 1000),
|
Math.round((Number(player.duration) || 0) * 1000)
|
||||||
);
|
);
|
||||||
const positionMs = Math.max(
|
const positionMs = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.round((Number(player.currentTime) || 0) * 1000),
|
Math.round((Number(player.currentTime) || 0) * 1000)
|
||||||
);
|
);
|
||||||
setProgressInfo((prev) => {
|
setProgressInfo((prev) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -317,10 +317,7 @@ const CreateLyricsWithAi = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPressBack = () => {
|
const onPressBack = () => {
|
||||||
// If currently on SongStructure, go back to previous screen instead of previous step
|
if (selectedIndex > 0) {
|
||||||
if (selectedIndex === 5) {
|
|
||||||
goBack();
|
|
||||||
} else if (selectedIndex > 0) {
|
|
||||||
setSelectedIndex((idx) => Math.max(0, idx - 1));
|
setSelectedIndex((idx) => Math.max(0, idx - 1));
|
||||||
} else {
|
} else {
|
||||||
goBack();
|
goBack();
|
||||||
|
|||||||
@@ -319,10 +319,7 @@ const CreateLyricsWithAi = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPressBack = () => {
|
const onPressBack = () => {
|
||||||
// If currently on SongStructure, go back to previous screen instead of previous step
|
if (selectedIndex > 0) {
|
||||||
if (selectedIndex === 5) {
|
|
||||||
goBack();
|
|
||||||
} else if (selectedIndex > 0) {
|
|
||||||
setSelectedIndex((idx) => Math.max(0, idx - 1));
|
setSelectedIndex((idx) => Math.max(0, idx - 1));
|
||||||
} else {
|
} else {
|
||||||
goBack();
|
goBack();
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ const PALETTE_SEGMENT_OVERRIDES = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getPaletteSegmentInfo = (type) => {
|
||||||
|
const meta = getSegmentMeta(type);
|
||||||
|
const override = PALETTE_SEGMENT_OVERRIDES.find(
|
||||||
|
(segment) => segment.type === type
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
label: meta?.label || formatStructureLabel(type, 1),
|
||||||
|
description: override?.description || "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -163,6 +174,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
);
|
);
|
||||||
const [activeItemId, setActiveItemId] = useState(null);
|
const [activeItemId, setActiveItemId] = useState(null);
|
||||||
const [insertPreviewIndex, setInsertPreviewIndex] = useState(null);
|
const [insertPreviewIndex, setInsertPreviewIndex] = useState(null);
|
||||||
|
const [dragOverlay, setDragOverlay] = useState(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const sortableItemsRef = useRef(sortableItems);
|
const sortableItemsRef = useRef(sortableItems);
|
||||||
const dragStateRef = useRef({
|
const dragStateRef = useRef({
|
||||||
@@ -175,8 +187,36 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
isOverContainer: false,
|
isOverContainer: false,
|
||||||
startItems: null,
|
startItems: null,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
|
startIndex: null,
|
||||||
|
slotIndex: null,
|
||||||
|
grabOffsetX: null,
|
||||||
|
grabOffsetY: null,
|
||||||
});
|
});
|
||||||
const globalListenersRef = useRef({ move: null, up: null });
|
const globalListenersRef = useRef({ move: null, up: null });
|
||||||
|
const clearDragOverlay = useCallback(() => {
|
||||||
|
setDragOverlay(null);
|
||||||
|
}, []);
|
||||||
|
const updateDragOverlayPosition = useCallback((clientX, clientY) => {
|
||||||
|
setDragOverlay((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const offsetX =
|
||||||
|
typeof dragStateRef.current?.grabOffsetX === "number"
|
||||||
|
? dragStateRef.current.grabOffsetX
|
||||||
|
: prev.width / 2;
|
||||||
|
const offsetY =
|
||||||
|
typeof dragStateRef.current?.grabOffsetY === "number"
|
||||||
|
? dragStateRef.current.grabOffsetY
|
||||||
|
: prev.height / 2;
|
||||||
|
const left =
|
||||||
|
typeof clientX === "number" ? clientX - offsetX : prev.left;
|
||||||
|
const top =
|
||||||
|
typeof clientY === "number" ? clientY - offsetY : prev.top;
|
||||||
|
if (left === prev.left && top === prev.top) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { ...prev, left, top };
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
const showTooltip = useCallback(
|
const showTooltip = useCallback(
|
||||||
(text, type = "error") => {
|
(text, type = "error") => {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
@@ -229,14 +269,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
const paletteSegments = useMemo(
|
const paletteSegments = useMemo(
|
||||||
() =>
|
() =>
|
||||||
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
||||||
const meta = getSegmentMeta(type);
|
const info = getPaletteSegmentInfo(type);
|
||||||
const override = PALETTE_SEGMENT_OVERRIDES.find(
|
|
||||||
(segment) => segment.type === type
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
label: meta?.label || formatStructureLabel(type, 1),
|
label: info.label,
|
||||||
description: override?.description || "",
|
description: info.description,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
@@ -352,6 +389,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
isOverContainer: false,
|
isOverContainer: false,
|
||||||
startItems: null,
|
startItems: null,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
|
startIndex: null,
|
||||||
|
slotIndex: null,
|
||||||
|
grabOffsetX: null,
|
||||||
|
grabOffsetY: null,
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -401,34 +442,77 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
: scrollY;
|
: scrollY;
|
||||||
const pointerY = absoluteY - (dragState.containerTop || 0);
|
const pointerY = absoluteY - (dragState.containerTop || 0);
|
||||||
|
|
||||||
|
updateDragOverlayPosition(event?.clientX, event?.clientY);
|
||||||
|
|
||||||
if (dragState.itemId) {
|
if (dragState.itemId) {
|
||||||
dragStateRef.current.blockedReason = null;
|
const rect = dragState.containerRect;
|
||||||
setSortableItems((prev) => {
|
const listLength = sortableItemsRef.current.length;
|
||||||
const currentIndex = prev.findIndex(
|
const isOver =
|
||||||
(it) => it.id === dragState.itemId
|
rect == null
|
||||||
);
|
? true
|
||||||
if (currentIndex === -1) return prev;
|
: event.clientX >= rect.left && event.clientX <= rect.right;
|
||||||
const maxIndex = Math.max(prev.length - 1, 0);
|
dragStateRef.current.isOverContainer = isOver;
|
||||||
const targetIndex = clamp(
|
|
||||||
Math.floor(pointerY / ROW_HEIGHT),
|
if (!isOver) {
|
||||||
0,
|
dragStateRef.current.targetIndex = null;
|
||||||
maxIndex
|
dragStateRef.current.slotIndex = null;
|
||||||
);
|
if (insertPreviewIndex !== null) {
|
||||||
const movingItem = prev[currentIndex];
|
setInsertPreviewIndex(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerHeight = rect?.height ?? listLength * ROW_HEIGHT;
|
||||||
|
let slotIndex = Math.round(pointerY / ROW_HEIGHT);
|
||||||
|
if (pointerY < 0) slotIndex = 0;
|
||||||
|
if (pointerY > containerHeight) slotIndex = listLength;
|
||||||
|
slotIndex = clamp(slotIndex, 0, listLength);
|
||||||
|
|
||||||
|
const snapshot = dragState.startItems || sortableItemsRef.current;
|
||||||
|
const movingIndex =
|
||||||
|
typeof dragState.startIndex === "number"
|
||||||
|
? dragState.startIndex
|
||||||
|
: snapshot.findIndex((item) => item.id === dragState.itemId);
|
||||||
|
dragStateRef.current.startIndex = movingIndex;
|
||||||
|
if (movingIndex < 0) {
|
||||||
|
dragStateRef.current.slotIndex = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movingItem = snapshot[movingIndex];
|
||||||
const movingType = movingItem?.type || movingItem?.value;
|
const movingType = movingItem?.type || movingItem?.value;
|
||||||
const topType = prev[0]?.type || prev[0]?.value;
|
const topType = snapshot[0]?.type || snapshot[0]?.value;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
targetIndex === 0 &&
|
slotIndex === 0 &&
|
||||||
INTRO_TYPES.has(topType) &&
|
INTRO_TYPES.has(topType) &&
|
||||||
!INTRO_TYPES.has(movingType)
|
!INTRO_TYPES.has(movingType)
|
||||||
) {
|
) {
|
||||||
dragStateRef.current.blockedReason = "intro_must_be_first";
|
dragStateRef.current.blockedReason = "intro_must_be_first";
|
||||||
return prev;
|
return;
|
||||||
}
|
}
|
||||||
if (targetIndex === currentIndex) return prev;
|
|
||||||
const reordered = reorder(prev, currentIndex, targetIndex);
|
dragStateRef.current.blockedReason = null;
|
||||||
return recalculateOccurrences(reordered);
|
const shouldPreview =
|
||||||
});
|
typeof movingIndex === "number" &&
|
||||||
|
slotIndex !== movingIndex &&
|
||||||
|
slotIndex !== movingIndex + 1;
|
||||||
|
if (shouldPreview) {
|
||||||
|
if (dragStateRef.current.slotIndex !== slotIndex) {
|
||||||
|
dragStateRef.current.slotIndex = slotIndex;
|
||||||
|
setInsertPreviewIndex(slotIndex);
|
||||||
|
}
|
||||||
|
} else if (insertPreviewIndex !== null) {
|
||||||
|
dragStateRef.current.slotIndex = null;
|
||||||
|
setInsertPreviewIndex(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetIndex = slotIndex;
|
||||||
|
if (typeof movingIndex === "number" && slotIndex > movingIndex) {
|
||||||
|
targetIndex -= 1;
|
||||||
|
}
|
||||||
|
targetIndex = clamp(targetIndex, 0, Math.max(listLength - 1, 0));
|
||||||
|
dragStateRef.current.targetIndex = targetIndex;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +546,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
setInsertPreviewIndex(targetIndex);
|
setInsertPreviewIndex(targetIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[insertPreviewIndex, setSortableItems]
|
[insertPreviewIndex, setInsertPreviewIndex, updateDragOverlayPosition]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGlobalPointerUp = useCallback(
|
const handleGlobalPointerUp = useCallback(
|
||||||
@@ -480,17 +564,36 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
if (dragState.itemId) {
|
if (dragState.itemId) {
|
||||||
if (dragState.blockedReason === "intro_must_be_first") {
|
if (dragState.blockedReason === "intro_must_be_first") {
|
||||||
showTooltip(INTRO_ERROR_MESSAGE);
|
showTooltip(INTRO_ERROR_MESSAGE);
|
||||||
}
|
} else if (
|
||||||
const validation = validateStructureRules(
|
dragState.isOverContainer &&
|
||||||
sortableItemsRef.current || []
|
typeof dragState.targetIndex === "number"
|
||||||
|
) {
|
||||||
|
const desiredIndex = dragState.targetIndex;
|
||||||
|
setSortableItems((prev) => {
|
||||||
|
const currentIndex = prev.findIndex(
|
||||||
|
(it) => it.id === dragState.itemId
|
||||||
);
|
);
|
||||||
|
if (currentIndex === -1) return prev;
|
||||||
|
const targetIndex = clamp(
|
||||||
|
desiredIndex,
|
||||||
|
0,
|
||||||
|
Math.max(prev.length, 0)
|
||||||
|
);
|
||||||
|
if (targetIndex === currentIndex) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const reordered = reorder(prev, currentIndex, targetIndex);
|
||||||
|
const recalculated = recalculateOccurrences(reordered);
|
||||||
|
const validation = validateStructureRules(recalculated);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
const fallbackItems =
|
const fallbackItems =
|
||||||
dragState.startItems?.map((item) => item) ||
|
dragState.startItems?.map((item) => item) ||
|
||||||
sortableItemsRef.current?.map((item) => item) ||
|
prev;
|
||||||
[];
|
|
||||||
setSortableItems(fallbackItems);
|
|
||||||
showTooltip(validation.message);
|
showTooltip(validation.message);
|
||||||
|
return recalculateOccurrences(fallbackItems);
|
||||||
|
}
|
||||||
|
return recalculated;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (dragState.paletteType) {
|
} else if (dragState.paletteType) {
|
||||||
let isOver = dragState.isOverContainer;
|
let isOver = dragState.isOverContainer;
|
||||||
@@ -532,9 +635,16 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
|
|
||||||
setActiveItemId(null);
|
setActiveItemId(null);
|
||||||
setInsertPreviewIndex(null);
|
setInsertPreviewIndex(null);
|
||||||
|
clearDragOverlay();
|
||||||
cleanupPointerListeners();
|
cleanupPointerListeners();
|
||||||
},
|
},
|
||||||
[addOptionalSegment, cleanupPointerListeners, showTooltip]
|
[
|
||||||
|
addOptionalSegment,
|
||||||
|
cleanupPointerListeners,
|
||||||
|
clearDragOverlay,
|
||||||
|
showTooltip,
|
||||||
|
setSortableItems,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const attachGlobalListeners = useCallback(() => {
|
const attachGlobalListeners = useCallback(() => {
|
||||||
@@ -555,7 +665,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
}, [handleGlobalPointerMove, handleGlobalPointerUp]);
|
}, [handleGlobalPointerMove, handleGlobalPointerUp]);
|
||||||
|
|
||||||
const startItemDrag = useCallback(
|
const startItemDrag = useCallback(
|
||||||
(itemId, event) => {
|
(itemId, event, dragElement = null) => {
|
||||||
const nativeEvent = event?.nativeEvent || {};
|
const nativeEvent = event?.nativeEvent || {};
|
||||||
if (typeof nativeEvent.button === "number" && nativeEvent.button !== 0)
|
if (typeof nativeEvent.button === "number" && nativeEvent.button !== 0)
|
||||||
return;
|
return;
|
||||||
@@ -571,6 +681,32 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
: scrollY;
|
: scrollY;
|
||||||
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
||||||
const snapshot = sortableItemsRef.current?.map((item) => item) || [];
|
const snapshot = sortableItemsRef.current?.map((item) => item) || [];
|
||||||
|
const startIndex = snapshot.findIndex((item) => item.id === itemId);
|
||||||
|
const movingItem = startIndex >= 0 ? snapshot[startIndex] : null;
|
||||||
|
const baseType = movingItem?.type || movingItem?.value;
|
||||||
|
const occurrence =
|
||||||
|
typeof movingItem?.occurrence === "number" ? movingItem.occurrence : 1;
|
||||||
|
const label =
|
||||||
|
baseType != null ? formatStructureLabel(baseType, occurrence) : "";
|
||||||
|
const pointerClientX =
|
||||||
|
typeof nativeEvent.clientX === "number" ? nativeEvent.clientX : 0;
|
||||||
|
const pointerClientY =
|
||||||
|
typeof nativeEvent.clientY === "number" ? nativeEvent.clientY : 0;
|
||||||
|
const getRect = (node) =>
|
||||||
|
typeof node?.getBoundingClientRect === "function"
|
||||||
|
? node.getBoundingClientRect()
|
||||||
|
: null;
|
||||||
|
const itemRect = getRect(dragElement) || getRect(event?.currentTarget);
|
||||||
|
const overlayWidth = itemRect?.width || rect?.width || 280;
|
||||||
|
const overlayHeight = itemRect?.height || ROW_HEIGHT;
|
||||||
|
const grabOffsetX = itemRect
|
||||||
|
? pointerClientX - itemRect.left
|
||||||
|
: overlayWidth / 2;
|
||||||
|
const grabOffsetY = itemRect
|
||||||
|
? pointerClientY - itemRect.top
|
||||||
|
: overlayHeight / 2;
|
||||||
|
const overlayLeft = pointerClientX - grabOffsetX;
|
||||||
|
const overlayTop = pointerClientY - grabOffsetY;
|
||||||
|
|
||||||
dragStateRef.current = {
|
dragStateRef.current = {
|
||||||
itemId,
|
itemId,
|
||||||
@@ -593,10 +729,23 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
isOverContainer: true,
|
isOverContainer: true,
|
||||||
startItems: snapshot,
|
startItems: snapshot,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
|
startIndex,
|
||||||
|
slotIndex: null,
|
||||||
|
grabOffsetX,
|
||||||
|
grabOffsetY,
|
||||||
};
|
};
|
||||||
|
|
||||||
setActiveItemId(itemId);
|
setActiveItemId(itemId);
|
||||||
setInsertPreviewIndex(null);
|
setInsertPreviewIndex(null);
|
||||||
|
setDragOverlay({
|
||||||
|
kind: "structure",
|
||||||
|
label,
|
||||||
|
description: "",
|
||||||
|
width: overlayWidth,
|
||||||
|
height: overlayHeight,
|
||||||
|
left: overlayLeft,
|
||||||
|
top: overlayTop,
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.body.style.userSelect = "none";
|
document.body.style.userSelect = "none";
|
||||||
@@ -624,6 +773,25 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
? nativeEvent.clientY + scrollY
|
? nativeEvent.clientY + scrollY
|
||||||
: scrollY;
|
: scrollY;
|
||||||
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
||||||
|
const pointerClientX =
|
||||||
|
typeof nativeEvent.clientX === "number" ? nativeEvent.clientX : 0;
|
||||||
|
const pointerClientY =
|
||||||
|
typeof nativeEvent.clientY === "number" ? nativeEvent.clientY : 0;
|
||||||
|
const cardRect =
|
||||||
|
typeof event?.currentTarget?.getBoundingClientRect === "function"
|
||||||
|
? event.currentTarget.getBoundingClientRect()
|
||||||
|
: null;
|
||||||
|
const overlayWidth = cardRect?.width || 320;
|
||||||
|
const overlayHeight = cardRect?.height || ROW_HEIGHT;
|
||||||
|
const grabOffsetX = cardRect
|
||||||
|
? pointerClientX - cardRect.left
|
||||||
|
: overlayWidth / 2;
|
||||||
|
const grabOffsetY = cardRect
|
||||||
|
? pointerClientY - cardRect.top
|
||||||
|
: overlayHeight / 2;
|
||||||
|
const overlayLeft = pointerClientX - grabOffsetX;
|
||||||
|
const overlayTop = pointerClientY - grabOffsetY;
|
||||||
|
const segmentInfo = getPaletteSegmentInfo(type);
|
||||||
|
|
||||||
dragStateRef.current = {
|
dragStateRef.current = {
|
||||||
itemId: null,
|
itemId: null,
|
||||||
@@ -645,10 +813,23 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
targetIndex: null,
|
targetIndex: null,
|
||||||
isOverContainer: false,
|
isOverContainer: false,
|
||||||
blockedReason: null,
|
blockedReason: null,
|
||||||
|
startIndex: null,
|
||||||
|
slotIndex: null,
|
||||||
|
grabOffsetX,
|
||||||
|
grabOffsetY,
|
||||||
};
|
};
|
||||||
|
|
||||||
setActiveItemId(`palette-${type}`);
|
setActiveItemId(`palette-${type}`);
|
||||||
setInsertPreviewIndex(null);
|
setInsertPreviewIndex(null);
|
||||||
|
setDragOverlay({
|
||||||
|
kind: "palette",
|
||||||
|
label: segmentInfo.label,
|
||||||
|
description: segmentInfo.description,
|
||||||
|
width: overlayWidth,
|
||||||
|
height: overlayHeight,
|
||||||
|
left: overlayLeft,
|
||||||
|
top: overlayTop,
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.body.style.userSelect = "none";
|
document.body.style.userSelect = "none";
|
||||||
@@ -665,17 +846,22 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
const meta = getSegmentMeta(baseType);
|
const meta = getSegmentMeta(baseType);
|
||||||
const canRemove = meta?.optional === true;
|
const canRemove = meta?.optional === true;
|
||||||
const isIntroduction = INTRO_TYPES.has(baseType);
|
const isIntroduction = INTRO_TYPES.has(baseType);
|
||||||
|
const rowRef = useRef(null);
|
||||||
|
const isActive = activeItemId === item.id;
|
||||||
const containerStyle = StyleSheet.flatten([
|
const containerStyle = StyleSheet.flatten([
|
||||||
styles.rowContainer,
|
styles.rowContainer,
|
||||||
activeItemId === item.id ? styles.activeItem : null,
|
isActive ? styles.activeItem : null,
|
||||||
|
isActive ? styles.draggingContainer : null,
|
||||||
]);
|
]);
|
||||||
const blurStyle = StyleSheet.flatten([
|
const blurStyle = StyleSheet.flatten([
|
||||||
styles.itemBlur,
|
styles.itemBlur,
|
||||||
activeItemId === item.id ? styles.activeBlur : null,
|
isActive ? styles.activeBlur : null,
|
||||||
|
isActive ? styles.draggingBlur : null,
|
||||||
]);
|
]);
|
||||||
const rowStyle = StyleSheet.flatten([
|
const rowStyle = StyleSheet.flatten([
|
||||||
styles.row,
|
styles.row,
|
||||||
isIntroduction ? styles.rowDisabled : styles.rowDraggable,
|
isIntroduction ? styles.rowDisabled : styles.rowDraggable,
|
||||||
|
isActive ? styles.rowHidden : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -687,10 +873,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
blurViewStyle={blurStyle}
|
blurViewStyle={blurStyle}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
ref={rowRef}
|
||||||
style={rowStyle}
|
style={rowStyle}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
if (isIntroduction) return;
|
if (isIntroduction) return;
|
||||||
startItemDrag(item.id, event);
|
startItemDrag(item.id, event, rowRef.current);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.rowText}>{item.label}</Text>
|
<Text style={styles.rowText}>{item.label}</Text>
|
||||||
@@ -714,7 +901,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
style={styles.handleWrapper}
|
style={styles.handleWrapper}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
event?.stopPropagation?.();
|
event?.stopPropagation?.();
|
||||||
startItemDrag(item.id, event);
|
startItemDrag(item.id, event, rowRef.current);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||||
@@ -848,6 +1035,52 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
{dragOverlay ? (
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={StyleSheet.flatten([
|
||||||
|
styles.dragOverlay,
|
||||||
|
{
|
||||||
|
width: dragOverlay.width || 320,
|
||||||
|
height: dragOverlay.height || ROW_HEIGHT,
|
||||||
|
left: dragOverlay.left || 0,
|
||||||
|
top: dragOverlay.top || 0,
|
||||||
|
},
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
<CreateLyricsHeader
|
||||||
|
tint="dark"
|
||||||
|
intensity={40}
|
||||||
|
showBorder={false}
|
||||||
|
containerStyle={StyleSheet.flatten([
|
||||||
|
styles.rowContainer,
|
||||||
|
styles.dragOverlayContainer,
|
||||||
|
])}
|
||||||
|
blurViewStyle={StyleSheet.flatten([
|
||||||
|
styles.itemBlur,
|
||||||
|
dragOverlay.kind === "palette" ? styles.paletteItemBlur : null,
|
||||||
|
styles.dragOverlayBlur,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{dragOverlay.kind === "palette" ? (
|
||||||
|
<View style={styles.paletteRow}>
|
||||||
|
<View style={styles.paletteTextWrapper}>
|
||||||
|
<Text style={styles.rowText}>{dragOverlay.label}</Text>
|
||||||
|
{dragOverlay.description ? (
|
||||||
|
<Text style={styles.paletteDescription}>
|
||||||
|
{dragOverlay.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.rowText}>{dragOverlay.label}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</CreateLyricsHeader>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -918,6 +1151,9 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: Palette.white,
|
borderColor: Palette.white,
|
||||||
},
|
},
|
||||||
|
draggingContainer: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
itemBlur: {
|
itemBlur: {
|
||||||
minHeight: ROW_HEIGHT,
|
minHeight: ROW_HEIGHT,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
@@ -934,6 +1170,9 @@ const styles = StyleSheet.create({
|
|||||||
activeBlur: {
|
activeBlur: {
|
||||||
backgroundColor: Palette.transparentWhite,
|
backgroundColor: Palette.transparentWhite,
|
||||||
},
|
},
|
||||||
|
draggingBlur: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
row: {
|
row: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -946,6 +1185,9 @@ const styles = StyleSheet.create({
|
|||||||
rowDisabled: {
|
rowDisabled: {
|
||||||
cursor: "default",
|
cursor: "default",
|
||||||
},
|
},
|
||||||
|
rowHidden: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
rowText: {
|
rowText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
@@ -1033,4 +1275,21 @@ const styles = StyleSheet.create({
|
|||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
fontFamily: FONT_FAMILY.InterMedium,
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
},
|
},
|
||||||
|
dragOverlay: {
|
||||||
|
position: "fixed",
|
||||||
|
zIndex: 999,
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
|
dragOverlayContainer: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
dragOverlayBlur: {
|
||||||
|
cursor: "grabbing",
|
||||||
|
opacity: 1,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOpacity: 0.35,
|
||||||
|
shadowRadius: 12,
|
||||||
|
shadowOffset: { width: 0, height: 8 },
|
||||||
|
elevation: 6,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user