feat: add drag and drop errors
This commit is contained in:
@@ -23,6 +23,7 @@ import SongTo from "./SongTo";
|
||||
import SpecificityContext from "./SpecificityContext";
|
||||
const { width: windowWidth } = Dimensions.get("window");
|
||||
const MAX_STEP_INDEX = 8;
|
||||
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false };
|
||||
|
||||
const CreateLyricsWithAi = () => {
|
||||
const { isWeb } = useLayoutType();
|
||||
@@ -140,10 +141,10 @@ const CreateLyricsWithAi = () => {
|
||||
|
||||
// Structure personnalisée: prioriser selections.customStructure puis config.structure
|
||||
const savedCustom = Array.isArray(sel.customStructure)
|
||||
? sanitizeStructureList(sel.customStructure)
|
||||
? sanitizeStructureList(sel.customStructure, CUSTOM_SANITIZE_OPTIONS)
|
||||
: null;
|
||||
const cfgStructure = Array.isArray(cfg.structure)
|
||||
? sanitizeStructureList(cfg.structure)
|
||||
? sanitizeStructureList(cfg.structure, CUSTOM_SANITIZE_OPTIONS)
|
||||
: null;
|
||||
const fallbackStructure =
|
||||
savedCustom && savedCustom.length ? savedCustom : cfgStructure;
|
||||
@@ -196,7 +197,7 @@ const CreateLyricsWithAi = () => {
|
||||
: style || undefined;
|
||||
const sanitizedCustom =
|
||||
Array.isArray(customStructure) && customStructure.length > 0
|
||||
? sanitizeStructureList(customStructure)
|
||||
? sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS)
|
||||
: [];
|
||||
const sanitizedParsed = Array.isArray(parsedStructure)
|
||||
? sanitizeStructureList(parsedStructure)
|
||||
|
||||
@@ -21,6 +21,7 @@ import SongTo from "./SongTo";
|
||||
import SpecificityContext from "./SpecificityContext";
|
||||
|
||||
const MAX_STEP_INDEX = 8;
|
||||
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false };
|
||||
|
||||
const CreateLyricsWithAi = () => {
|
||||
const { selectedProject } = useUser();
|
||||
@@ -133,10 +134,10 @@ const CreateLyricsWithAi = () => {
|
||||
|
||||
// Structure personnalisée: prioriser selections.customStructure puis config.structure
|
||||
const savedCustom = Array.isArray(sel.customStructure)
|
||||
? sanitizeStructureList(sel.customStructure)
|
||||
? sanitizeStructureList(sel.customStructure, CUSTOM_SANITIZE_OPTIONS)
|
||||
: null;
|
||||
const cfgStructure = Array.isArray(cfg.structure)
|
||||
? sanitizeStructureList(cfg.structure)
|
||||
? sanitizeStructureList(cfg.structure, CUSTOM_SANITIZE_OPTIONS)
|
||||
: null;
|
||||
const fallbackStructure =
|
||||
savedCustom && savedCustom.length ? savedCustom : cfgStructure;
|
||||
@@ -201,7 +202,7 @@ const CreateLyricsWithAi = () => {
|
||||
: style || undefined;
|
||||
const sanitizedCustom =
|
||||
Array.isArray(customStructure) && customStructure.length > 0
|
||||
? sanitizeStructureList(customStructure)
|
||||
? sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS)
|
||||
: [];
|
||||
const sanitizedParsed = Array.isArray(parsedStructure)
|
||||
? sanitizeStructureList(parsedStructure)
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
|
||||
const FAKE_PROGRESS_MAX = 96;
|
||||
const PROGRESS_INTERVAL_MS = 250;
|
||||
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false };
|
||||
|
||||
const CreatingLyrics = ({ active, config, selections }) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
@@ -188,7 +189,8 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
if (persistedSelections) {
|
||||
if ("customStructure" in persistedSelections) {
|
||||
persistedSelections.customStructure = sanitizeStructureList(
|
||||
persistedSelections.customStructure
|
||||
persistedSelections.customStructure,
|
||||
CUSTOM_SANITIZE_OPTIONS
|
||||
);
|
||||
}
|
||||
if ("parsedStructure" in persistedSelections) {
|
||||
|
||||
@@ -28,16 +28,17 @@ const createSortableItem = (type, index = 0) => ({
|
||||
type,
|
||||
value: type,
|
||||
});
|
||||
const SANITIZE_OPTIONS = { autoInjectPreChorus: false };
|
||||
|
||||
const buildSortableItems = (values = []) =>
|
||||
sanitizeStructureList(values).map((type, index) =>
|
||||
sanitizeStructureList(values, SANITIZE_OPTIONS).map((type, index) =>
|
||||
createSortableItem(type, index)
|
||||
);
|
||||
|
||||
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
const scrollRef = useAnimatedRef();
|
||||
const sanitizedBase = useMemo(
|
||||
() => sanitizeStructureList(baseStructure),
|
||||
() => sanitizeStructureList(baseStructure, SANITIZE_OPTIONS),
|
||||
[baseStructure]
|
||||
);
|
||||
const extractValues = useCallback(
|
||||
@@ -100,7 +101,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
(itemId) => {
|
||||
setSortableItems((prev) => {
|
||||
const remaining = prev.filter((item) => item.id !== itemId);
|
||||
const sanitized = sanitizeStructureList(extractValues(remaining));
|
||||
const sanitized = sanitizeStructureList(
|
||||
extractValues(remaining),
|
||||
SANITIZE_OPTIONS
|
||||
);
|
||||
return buildSortableItems(sanitized);
|
||||
});
|
||||
},
|
||||
@@ -124,12 +128,12 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
const allowsMultiple = meta?.allowMultiple !== false;
|
||||
if (!allowsMultiple && nextValues.includes(type)) {
|
||||
const filtered = nextValues.filter((value) => value !== type);
|
||||
const sanitized = sanitizeStructureList(filtered);
|
||||
const sanitized = sanitizeStructureList(filtered, SANITIZE_OPTIONS);
|
||||
return buildSortableItems(sanitized);
|
||||
}
|
||||
|
||||
nextValues.push(type);
|
||||
const sanitized = sanitizeStructureList(nextValues);
|
||||
const sanitized = sanitizeStructureList(nextValues, SANITIZE_OPTIONS);
|
||||
return buildSortableItems(sanitized);
|
||||
});
|
||||
},
|
||||
@@ -219,7 +223,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
scrollableRef={scrollRef}
|
||||
onDragEnd={({ data: newData }) => {
|
||||
const values = extractValues(newData);
|
||||
const sanitized = sanitizeStructureList(values);
|
||||
const sanitized = sanitizeStructureList(
|
||||
values,
|
||||
SANITIZE_OPTIONS
|
||||
);
|
||||
setSortableItems(buildSortableItems(sanitized));
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useGlobal } from "reactn";
|
||||
import { icons } from "../../assets";
|
||||
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import {
|
||||
@@ -24,10 +26,21 @@ import {
|
||||
OPTIONAL_STRUCTURE_SEGMENTS,
|
||||
sanitizeStructureList,
|
||||
} from "../../utils/songStructure";
|
||||
import ItemContainer from "../../components/ItemContainer/ItemContainer";
|
||||
import CreateLyricsHeader from "./components/CreateLyricsHeader";
|
||||
|
||||
const ROW_HEIGHT = 54;
|
||||
const PRE_REFRAIN_TYPE = "pre_refrain_instrumental";
|
||||
const INTRO_TYPES = new Set([
|
||||
"short_intro",
|
||||
"long_intro",
|
||||
"intro",
|
||||
"introduction",
|
||||
]);
|
||||
const PRE_REFRAIN_ERROR_MESSAGE =
|
||||
"Impossible de positionner un pré-refrain ici. Place-le juste avant un refrain.";
|
||||
const INTRO_ERROR_MESSAGE =
|
||||
"L'introduction musicale doit rester en première position.";
|
||||
const SANITIZE_OPTIONS = { autoInjectPreChorus: false };
|
||||
|
||||
const PALETTE_SEGMENT_OVERRIDES = [
|
||||
{
|
||||
@@ -56,16 +69,72 @@ 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()}`,
|
||||
const createSortableItem = (type, occurrence = 1) => ({
|
||||
id: `${type}-${occurrence}-${randomSuffix()}`,
|
||||
type,
|
||||
value: type,
|
||||
occurrence,
|
||||
});
|
||||
|
||||
const buildSortableItems = (values = []) =>
|
||||
sanitizeStructureList(values).map((type, index) =>
|
||||
createSortableItem(type, index)
|
||||
const buildSortableItems = (values = []) => {
|
||||
const counts = {};
|
||||
return sanitizeStructureList(values, SANITIZE_OPTIONS).map((type) => {
|
||||
const occurrence = (counts[type] || 0) + 1;
|
||||
counts[type] = occurrence;
|
||||
return createSortableItem(type, occurrence);
|
||||
});
|
||||
};
|
||||
|
||||
const getMaxOccurrence = (items = [], type) =>
|
||||
items.reduce((max, item) => {
|
||||
if (item?.type !== type) return max;
|
||||
const value = typeof item?.occurrence === "number" ? item.occurrence : 0;
|
||||
return Math.max(max, value);
|
||||
}, 0);
|
||||
|
||||
const recalculateOccurrences = (items = []) => {
|
||||
const counts = {};
|
||||
return items.map((item) => {
|
||||
const type = item?.type || item?.value;
|
||||
const occurrence = (counts[type] || 0) + 1;
|
||||
counts[type] = occurrence;
|
||||
if (item?.occurrence === occurrence) {
|
||||
return item;
|
||||
}
|
||||
return { ...item, occurrence };
|
||||
});
|
||||
};
|
||||
|
||||
const validateStructureRules = (items = []) => {
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const current = items[index];
|
||||
const type = current?.type || current?.value;
|
||||
if (type === PRE_REFRAIN_TYPE) {
|
||||
const next = items[index + 1];
|
||||
const nextType = next?.type || next?.value;
|
||||
if (nextType !== "refrain") {
|
||||
return {
|
||||
valid: false,
|
||||
reason: "pre_refrain_invalid_position",
|
||||
message: PRE_REFRAIN_ERROR_MESSAGE,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const introIndex = items.findIndex((item) =>
|
||||
INTRO_TYPES.has(item?.type || item?.value)
|
||||
);
|
||||
if (introIndex > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: "intro_must_be_first",
|
||||
message: INTRO_ERROR_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
const reorder = (list, from, to) => {
|
||||
if (from === to) return list;
|
||||
@@ -78,8 +147,9 @@ const reorder = (list, from, to) => {
|
||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||
|
||||
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const sanitizedBase = useMemo(
|
||||
() => sanitizeStructureList(baseStructure),
|
||||
() => sanitizeStructureList(baseStructure, SANITIZE_OPTIONS),
|
||||
[baseStructure]
|
||||
);
|
||||
const [sortableItems, setSortableItems] = useState(() =>
|
||||
@@ -97,8 +167,17 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
containerRect: null,
|
||||
targetIndex: null,
|
||||
isOverContainer: false,
|
||||
startItems: null,
|
||||
blockedReason: null,
|
||||
});
|
||||
const globalListenersRef = useRef({ move: null, up: null });
|
||||
const showTooltip = useCallback(
|
||||
(text, type = "error") => {
|
||||
if (!text) return;
|
||||
setTooltip({ text, type });
|
||||
},
|
||||
[setTooltip]
|
||||
);
|
||||
|
||||
const extractValues = useCallback(
|
||||
(items) =>
|
||||
@@ -121,23 +200,25 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
}, [sortableItems]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(extractValues(sortableItems));
|
||||
const values = extractValues(sortableItems);
|
||||
onChange?.(sanitizeStructureList(values, SANITIZE_OPTIONS));
|
||||
}, [sortableItems, extractValues, onChange]);
|
||||
|
||||
const labeledItems = useMemo(() => {
|
||||
const counts = {};
|
||||
return sortableItems.map((item) => {
|
||||
const labeledItems = useMemo(
|
||||
() =>
|
||||
sortableItems.map((item) => {
|
||||
const baseType = item.type || item.value;
|
||||
const nextCount = (counts[baseType] || 0) + 1;
|
||||
counts[baseType] = nextCount;
|
||||
const occurrence =
|
||||
typeof item.occurrence === "number" ? item.occurrence : 1;
|
||||
return {
|
||||
...item,
|
||||
type: baseType,
|
||||
value: baseType,
|
||||
label: formatStructureLabel(baseType, nextCount),
|
||||
label: formatStructureLabel(baseType, occurrence),
|
||||
};
|
||||
});
|
||||
}, [sortableItems]);
|
||||
}),
|
||||
[sortableItems]
|
||||
);
|
||||
|
||||
const paletteSegments = useMemo(
|
||||
() =>
|
||||
@@ -163,46 +244,95 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
return counts;
|
||||
}, [sortableItems, extractValues]);
|
||||
|
||||
const handleRemoveItem = useCallback(
|
||||
(itemId) => {
|
||||
const handleRemoveItem = useCallback((itemId) => {
|
||||
setSortableItems((prev) => {
|
||||
const remaining = prev.filter((item) => item.id !== itemId);
|
||||
const sanitized = sanitizeStructureList(extractValues(remaining));
|
||||
return buildSortableItems(sanitized);
|
||||
return recalculateOccurrences(remaining);
|
||||
});
|
||||
},
|
||||
[extractValues]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const addOptionalSegment = useCallback(
|
||||
(type, insertIndex = null) => {
|
||||
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;
|
||||
});
|
||||
if (!meta) {
|
||||
return { success: false, reason: "unknown_segment" };
|
||||
}
|
||||
|
||||
if (meta?.allowMultiple === false) {
|
||||
nextValues = nextValues.filter((value) => value !== type);
|
||||
}
|
||||
|
||||
const targetIndex =
|
||||
const currentItems = sortableItemsRef.current || [];
|
||||
let targetIndex =
|
||||
typeof insertIndex === "number"
|
||||
? clamp(insertIndex, 0, nextValues.length)
|
||||
: nextValues.length;
|
||||
? clamp(insertIndex, 0, currentItems.length)
|
||||
: currentItems.length;
|
||||
|
||||
nextValues.splice(targetIndex, 0, type);
|
||||
const sanitized = sanitizeStructureList(nextValues);
|
||||
return buildSortableItems(sanitized);
|
||||
const nextItems = [];
|
||||
let reusedOccurrence = null;
|
||||
|
||||
currentItems.forEach((item, index) => {
|
||||
const itemMeta = getSegmentMeta(item?.type);
|
||||
const conflictsExclusive =
|
||||
meta?.exclusiveGroup &&
|
||||
itemMeta?.exclusiveGroup === meta.exclusiveGroup;
|
||||
const conflictsSameType =
|
||||
meta?.allowMultiple === false && item?.type === type;
|
||||
|
||||
if (conflictsExclusive || conflictsSameType) {
|
||||
if (index < targetIndex) {
|
||||
targetIndex -= 1;
|
||||
}
|
||||
if (item?.type === type && reusedOccurrence == null) {
|
||||
reusedOccurrence =
|
||||
typeof item?.occurrence === "number" ? item.occurrence : 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nextItems.push(item);
|
||||
});
|
||||
|
||||
if (
|
||||
type === PRE_REFRAIN_TYPE &&
|
||||
insertIndex == null &&
|
||||
nextItems.length > 0
|
||||
) {
|
||||
const firstRefrainIndex = nextItems.findIndex(
|
||||
(item) => item?.type === "refrain"
|
||||
);
|
||||
if (firstRefrainIndex !== -1) {
|
||||
targetIndex = firstRefrainIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (meta?.exclusiveGroup === "intro") {
|
||||
targetIndex = 0;
|
||||
} else if (meta?.exclusiveGroup === "outro") {
|
||||
targetIndex = nextItems.length;
|
||||
}
|
||||
|
||||
const occurrence =
|
||||
meta?.allowMultiple === false
|
||||
? reusedOccurrence ?? 1
|
||||
: getMaxOccurrence(nextItems, type) + 1;
|
||||
|
||||
const insertionIndex = clamp(targetIndex, 0, nextItems.length);
|
||||
const draftedItems = [...nextItems];
|
||||
draftedItems.splice(
|
||||
insertionIndex,
|
||||
0,
|
||||
createSortableItem(type, occurrence)
|
||||
);
|
||||
|
||||
const recalculatedDraft = recalculateOccurrences(draftedItems);
|
||||
|
||||
const validation = validateStructureRules(recalculatedDraft);
|
||||
if (!validation.valid) {
|
||||
showTooltip(validation.message);
|
||||
return { success: false, reason: validation.reason };
|
||||
}
|
||||
|
||||
setSortableItems(recalculatedDraft);
|
||||
return { success: true };
|
||||
},
|
||||
[extractValues]
|
||||
[showTooltip]
|
||||
);
|
||||
|
||||
const resetDragState = useCallback(() => {
|
||||
@@ -214,6 +344,8 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
containerRect: null,
|
||||
targetIndex: null,
|
||||
isOverContainer: false,
|
||||
startItems: null,
|
||||
blockedReason: null,
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -264,6 +396,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
const pointerY = absoluteY - (dragState.containerTop || 0);
|
||||
|
||||
if (dragState.itemId) {
|
||||
dragStateRef.current.blockedReason = null;
|
||||
setSortableItems((prev) => {
|
||||
const currentIndex = prev.findIndex(
|
||||
(it) => it.id === dragState.itemId
|
||||
@@ -275,8 +408,20 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
0,
|
||||
maxIndex
|
||||
);
|
||||
const movingItem = prev[currentIndex];
|
||||
const movingType = movingItem?.type || movingItem?.value;
|
||||
const topType = prev[0]?.type || prev[0]?.value;
|
||||
if (
|
||||
targetIndex === 0 &&
|
||||
INTRO_TYPES.has(topType) &&
|
||||
!INTRO_TYPES.has(movingType)
|
||||
) {
|
||||
dragStateRef.current.blockedReason = "intro_must_be_first";
|
||||
return prev;
|
||||
}
|
||||
if (targetIndex === currentIndex) return prev;
|
||||
return reorder(prev, currentIndex, targetIndex);
|
||||
const reordered = reorder(prev, currentIndex, targetIndex);
|
||||
return recalculateOccurrences(reordered);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -327,9 +472,20 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
}
|
||||
|
||||
if (dragState.itemId) {
|
||||
setSortableItems((prev) =>
|
||||
buildSortableItems(extractValues(prev))
|
||||
if (dragState.blockedReason === "intro_must_be_first") {
|
||||
showTooltip(INTRO_ERROR_MESSAGE);
|
||||
}
|
||||
const validation = validateStructureRules(
|
||||
sortableItemsRef.current || []
|
||||
);
|
||||
if (!validation.valid) {
|
||||
const fallbackItems =
|
||||
dragState.startItems?.map((item) => item) ||
|
||||
sortableItemsRef.current?.map((item) => item) ||
|
||||
[];
|
||||
setSortableItems(fallbackItems);
|
||||
showTooltip(validation.message);
|
||||
}
|
||||
} else if (dragState.paletteType) {
|
||||
let isOver = dragState.isOverContainer;
|
||||
let targetIndex = dragState.targetIndex;
|
||||
@@ -363,6 +519,8 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
|
||||
if (isOver && typeof targetIndex === "number") {
|
||||
addOptionalSegment(dragState.paletteType, targetIndex);
|
||||
} else if (dragState.paletteType === PRE_REFRAIN_TYPE) {
|
||||
showTooltip(PRE_REFRAIN_ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +528,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
setInsertPreviewIndex(null);
|
||||
cleanupPointerListeners();
|
||||
},
|
||||
[addOptionalSegment, cleanupPointerListeners, extractValues]
|
||||
[addOptionalSegment, cleanupPointerListeners, showTooltip]
|
||||
);
|
||||
|
||||
const attachGlobalListeners = useCallback(() => {
|
||||
@@ -406,6 +564,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
? nativeEvent.clientY + scrollY
|
||||
: scrollY;
|
||||
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
||||
const snapshot = sortableItemsRef.current?.map((item) => item) || [];
|
||||
|
||||
dragStateRef.current = {
|
||||
itemId,
|
||||
@@ -426,6 +585,8 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
: null,
|
||||
targetIndex: null,
|
||||
isOverContainer: true,
|
||||
startItems: snapshot,
|
||||
blockedReason: null,
|
||||
};
|
||||
|
||||
setActiveItemId(itemId);
|
||||
@@ -477,6 +638,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
: null,
|
||||
targetIndex: null,
|
||||
isOverContainer: false,
|
||||
blockedReason: null,
|
||||
};
|
||||
|
||||
setActiveItemId(`palette-${type}`);
|
||||
@@ -496,11 +658,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
const baseType = item?.type || item?.value;
|
||||
const meta = getSegmentMeta(baseType);
|
||||
const canRemove = meta?.optional === true;
|
||||
const isIntroduction =
|
||||
baseType === "short_intro" ||
|
||||
baseType === "long_intro" ||
|
||||
baseType === "intro" ||
|
||||
baseType === "introduction";
|
||||
const isIntroduction = INTRO_TYPES.has(baseType);
|
||||
const containerStyle = StyleSheet.flatten([
|
||||
styles.rowContainer,
|
||||
activeItemId === item.id ? styles.activeItem : null,
|
||||
@@ -509,6 +667,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
styles.itemBlur,
|
||||
activeItemId === item.id ? styles.activeBlur : null,
|
||||
]);
|
||||
const rowStyle = StyleSheet.flatten([
|
||||
styles.row,
|
||||
isIntroduction ? styles.rowDisabled : styles.rowDraggable,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CreateLyricsHeader
|
||||
@@ -518,12 +680,22 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
containerStyle={containerStyle}
|
||||
blurViewStyle={blurStyle}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View
|
||||
style={rowStyle}
|
||||
onPointerDown={(event) => {
|
||||
if (isIntroduction) return;
|
||||
startItemDrag(item.id, event);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.rowText}>{item.label}</Text>
|
||||
<View style={styles.rowActions}>
|
||||
{canRemove && (
|
||||
<TouchableOpacity
|
||||
onPress={() => handleRemoveItem(item.id)}
|
||||
onPressIn={(ev) => ev?.stopPropagation?.()}
|
||||
onPress={(ev) => {
|
||||
ev?.stopPropagation?.();
|
||||
handleRemoveItem(item.id);
|
||||
}}
|
||||
style={styles.removeButton}
|
||||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||
>
|
||||
@@ -533,7 +705,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
{!isIntroduction && (
|
||||
<View
|
||||
style={styles.handleWrapper}
|
||||
onPointerDown={(event) => startItemDrag(item.id, event)}
|
||||
onPointerDown={(event) => {
|
||||
event?.stopPropagation?.();
|
||||
startItemDrag(item.id, event);
|
||||
}}
|
||||
>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</View>
|
||||
@@ -633,9 +808,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
onPressIn={(ev) => ev?.stopPropagation?.()}
|
||||
onPress={(ev) => {
|
||||
ev?.stopPropagation?.();
|
||||
addOptionalSegment(segment.type);
|
||||
const result = addOptionalSegment(segment.type);
|
||||
if (result?.success) {
|
||||
setInsertPreviewIndex(null);
|
||||
setActiveItemId(null);
|
||||
}
|
||||
}}
|
||||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||
style={({ pressed }) =>
|
||||
@@ -645,7 +822,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
])
|
||||
}
|
||||
>
|
||||
<Image source={icons.add} style={styles.addIcon} />
|
||||
<Image
|
||||
source={icons.add}
|
||||
style={styles.addIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<View style={badgeStyle}>
|
||||
<Text style={styles.countBadgeText}>{count}</Text>
|
||||
@@ -753,6 +933,12 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
rowDraggable: {
|
||||
cursor: "grab",
|
||||
},
|
||||
rowDisabled: {
|
||||
cursor: "default",
|
||||
},
|
||||
rowText: {
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
|
||||
@@ -284,8 +284,9 @@ export const normalizeStructureType = (value) => {
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
export const sanitizeStructureList = (structure = []) => {
|
||||
export const sanitizeStructureList = (structure = [], options = {}) => {
|
||||
if (!Array.isArray(structure)) return [];
|
||||
const { autoInjectPreChorus = true } = options || {};
|
||||
const output = [];
|
||||
let shouldInjectInstrumentalPreChorus = false;
|
||||
|
||||
@@ -311,7 +312,7 @@ export const sanitizeStructureList = (structure = []) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "pre_refrain_instrumental") {
|
||||
if (type === "pre_refrain_instrumental" && autoInjectPreChorus) {
|
||||
shouldInjectInstrumentalPreChorus = true;
|
||||
return;
|
||||
}
|
||||
@@ -321,7 +322,7 @@ export const sanitizeStructureList = (structure = []) => {
|
||||
|
||||
let result = output;
|
||||
|
||||
if (shouldInjectInstrumentalPreChorus) {
|
||||
if (autoInjectPreChorus && shouldInjectInstrumentalPreChorus) {
|
||||
const expanded = [];
|
||||
let hasRefrain = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user