new song structure
This commit is contained in:
@@ -363,7 +363,7 @@ const MusicDetails = ({ route }) => {
|
||||
let type = "section";
|
||||
if (lower.includes("refrain") || lower.includes("chorus")) type = "refrain";
|
||||
else if (lower.includes("couplet") || lower.includes("verse")) type = "couplet";
|
||||
else if (lower.includes("bridge")) type = "bridge";
|
||||
else if (lower.includes("pont") || lower.includes("bridge")) type = "pont";
|
||||
else if (lower.includes("intro")) type = "intro";
|
||||
const indexMatch = label.match(/(\d+)/);
|
||||
const index = indexMatch ? Number(indexMatch[1]) : undefined;
|
||||
@@ -373,7 +373,7 @@ const MusicDetails = ({ route }) => {
|
||||
if (fallback) return fallback;
|
||||
if (type === "refrain") return index > 1 ? `Refrain ${index}` : "Refrain";
|
||||
if (type === "couplet") return index > 1 ? `Couplet ${index}` : "Couplet";
|
||||
if (type === "bridge") return index > 1 ? `Pont ${index}` : "Pont";
|
||||
if (type === "pont") return index > 1 ? `Pont ${index}` : "Pont";
|
||||
if (type === "intro") return "Intro";
|
||||
return index > 1 ? `Section ${index}` : "Section";
|
||||
};
|
||||
|
||||
@@ -445,7 +445,7 @@ const MusicDetails = ({ route }) => {
|
||||
lower.includes("pre-refrain")
|
||||
)
|
||||
type = "pre_chorus";
|
||||
else if (lower.includes("bridge")) type = "bridge";
|
||||
else if (lower.includes("pont") || lower.includes("bridge")) type = "pont";
|
||||
else if (lower.includes("intro")) {
|
||||
if (lower.includes("long")) type = "long_intro";
|
||||
else type = "short_intro";
|
||||
@@ -468,7 +468,7 @@ const MusicDetails = ({ route }) => {
|
||||
return index > 1 ? `Refrain ${index}` : "Refrain";
|
||||
if (normalized === "couplet")
|
||||
return index > 1 ? `Couplet ${index}` : "Couplet";
|
||||
if (type === "bridge") return index > 1 ? `Pont ${index}` : "Pont";
|
||||
if (type === "pont") return index > 1 ? `Pont ${index}` : "Pont";
|
||||
if (type === "intro") return index > 1 ? `Intro ${index}` : "Intro";
|
||||
return index > 1 ? `Section ${index}` : "Section";
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
const hasShownErrorAlert = useRef(false);
|
||||
const { setIsLoading } = useMinuit();
|
||||
const { updateProjectData } = useUser();
|
||||
const progressValue = Math.max(0, Math.min(100, Math.round(progress)));
|
||||
|
||||
// Reset when becomes active
|
||||
useEffect(() => {
|
||||
@@ -301,7 +302,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
: "Ton texte est\nen cours de création"}
|
||||
</Text>
|
||||
<View style={{ alignItems: "center", gap: 16 }}>
|
||||
<ProgressBar gradient progress={progress} />
|
||||
<ProgressBar gradient progress={progressValue} />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
@@ -309,7 +310,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{progress}%
|
||||
{progressValue}%
|
||||
</Text>
|
||||
</View>
|
||||
{!saved && result && !error && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
@@ -15,6 +15,7 @@ import { icons } from "../../assets";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import {
|
||||
arraysAreSame,
|
||||
formatStructureLabel,
|
||||
getSegmentMeta,
|
||||
OPTIONAL_STRUCTURE_SEGMENTS,
|
||||
@@ -40,20 +41,28 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
() => sanitizeStructureList(baseStructure),
|
||||
[baseStructure]
|
||||
);
|
||||
const extractValues = useCallback(
|
||||
(items) =>
|
||||
(items || []).map((item) => item?.type || item?.value).filter(Boolean),
|
||||
[]
|
||||
);
|
||||
const [sortableItems, setSortableItems] = useState(() =>
|
||||
buildSortableItems(sanitizedBase)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSortableItems(buildSortableItems(sanitizedBase));
|
||||
}, [sanitizedBase]);
|
||||
|
||||
setSortableItems((prev) => {
|
||||
const currentValues = extractValues(prev);
|
||||
if (arraysAreSame(currentValues, sanitizedBase)) {
|
||||
return prev;
|
||||
}
|
||||
return buildSortableItems(sanitizedBase);
|
||||
});
|
||||
}, [sanitizedBase, extractValues]);
|
||||
useEffect(() => {
|
||||
const values = sortableItems
|
||||
.map((item) => item?.type || item?.value)
|
||||
.filter(Boolean);
|
||||
const values = extractValues(sortableItems);
|
||||
onChange?.(values);
|
||||
}, [sortableItems, onChange]);
|
||||
}, [sortableItems, onChange, extractValues]);
|
||||
|
||||
const labeledItems = useMemo(() => {
|
||||
const counts = {};
|
||||
@@ -80,40 +89,63 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const selectedTypes = useMemo(() => {
|
||||
const set = new Set();
|
||||
sortableItems.forEach((item) => {
|
||||
if (item?.type) set.add(item.type);
|
||||
else if (item?.value) set.add(item.value);
|
||||
const segmentCounts = useMemo(() => {
|
||||
const counts = {};
|
||||
extractValues(sortableItems).forEach((type) => {
|
||||
counts[type] = (counts[type] || 0) + 1;
|
||||
});
|
||||
return set;
|
||||
}, [sortableItems]);
|
||||
return counts;
|
||||
}, [sortableItems, extractValues]);
|
||||
|
||||
const toggleOptionalSegment = (type) => {
|
||||
setSortableItems((prev) => {
|
||||
const currentValues = prev.map((item) => item.type || item.value);
|
||||
if (currentValues.includes(type)) {
|
||||
const filtered = currentValues.filter((value) => value !== type);
|
||||
return buildSortableItems(filtered);
|
||||
}
|
||||
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);
|
||||
let nextValues = [...currentValues];
|
||||
if (meta?.exclusiveGroup) {
|
||||
nextValues = nextValues.filter((value) => {
|
||||
const itemMeta = getSegmentMeta(value);
|
||||
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
|
||||
});
|
||||
}
|
||||
nextValues.push(type);
|
||||
return buildSortableItems(nextValues);
|
||||
});
|
||||
};
|
||||
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 Row = ({ item: rowData, onPress }) => {
|
||||
const isButton = typeof onPress === "function";
|
||||
const Wrapper = isButton ? TouchableOpacity : View;
|
||||
const selected = rowData?.selected;
|
||||
const icon = rowData?.icon;
|
||||
const baseType = rowData?.type || rowData?.value;
|
||||
const meta = getSegmentMeta(baseType);
|
||||
const canRemove = rowData?.sortable && meta?.optional === true;
|
||||
const count = rowData?.count ?? 0;
|
||||
return (
|
||||
<Wrapper
|
||||
onPress={onPress}
|
||||
@@ -139,14 +171,29 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
|
||||
</View>
|
||||
{rowData?.sortable ? (
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
<View style={styles.rowActions}>
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
{canRemove && (
|
||||
<TouchableOpacity
|
||||
onPress={() => handleRemoveItem(rowData.id)}
|
||||
style={styles.removeButton}
|
||||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||
>
|
||||
<Image source={icons.close} style={styles.removeIcon} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
source={icon || (selected ? icons.check : icons.add)}
|
||||
style={[styles.optionIcon]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.countBadge,
|
||||
count > 0 ? styles.countBadgeActive : null,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.countBadgeText}>{count}</Text>
|
||||
</View>
|
||||
)}
|
||||
</BlurView>
|
||||
</View>
|
||||
@@ -166,46 +213,50 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
contentContainerStyle={{ paddingVertical: 4, paddingBottom: 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<>
|
||||
<Sortable.Grid
|
||||
columns={1}
|
||||
rowGap={12}
|
||||
data={labeledItems}
|
||||
renderItem={({ item }) => <Row item={item} />}
|
||||
customHandle
|
||||
scrollableRef={scrollRef}
|
||||
onDragEnd={({ data: newData }) => {
|
||||
const values = (newData || [])
|
||||
.map((it) => it?.type || it?.value)
|
||||
.filter(Boolean);
|
||||
const sanitized = sanitizeStructureList(values);
|
||||
setSortableItems(buildSortableItems(sanitized));
|
||||
}}
|
||||
/>
|
||||
<FlatList
|
||||
data={optionalSegments}
|
||||
gap={12}
|
||||
renderItem={({ item }) => {
|
||||
const selected = selectedTypes.has(item.type);
|
||||
return (
|
||||
<View style={{ marginTop: 12 }}>
|
||||
<Row
|
||||
item={{
|
||||
...item,
|
||||
sortable: false,
|
||||
selected,
|
||||
}}
|
||||
onPress={() => toggleOptionalSegment(item.type)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
keyExtractor={(item) => item.type}
|
||||
contentContainerStyle={{ paddingBottom: 24 }}
|
||||
scrollEnabled={false}
|
||||
nestedScrollEnabled={false}
|
||||
/>
|
||||
</>
|
||||
<View style={styles.sectionTag}>
|
||||
<Text style={styles.sectionTagText}>Structure actuelle</Text>
|
||||
</View>
|
||||
<Sortable.Grid
|
||||
columns={1}
|
||||
rowGap={12}
|
||||
data={labeledItems}
|
||||
renderItem={({ item }) => <Row item={item} />}
|
||||
customHandle
|
||||
scrollableRef={scrollRef}
|
||||
onDragEnd={({ data: newData }) => {
|
||||
const values = extractValues(newData);
|
||||
const sanitized = sanitizeStructureList(values);
|
||||
setSortableItems(buildSortableItems(sanitized));
|
||||
}}
|
||||
/>
|
||||
<View style={[styles.sectionTag, { marginTop: 24 }]}>
|
||||
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={optionalSegments}
|
||||
gap={12}
|
||||
renderItem={({ item }) => {
|
||||
const count = segmentCounts[item.type] || 0;
|
||||
const selected = count > 0;
|
||||
return (
|
||||
<View style={{ marginTop: 12 }}>
|
||||
<Row
|
||||
item={{
|
||||
...item,
|
||||
sortable: false,
|
||||
selected,
|
||||
count,
|
||||
}}
|
||||
onPress={() => handleOptionalSegmentPress(item.type)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
keyExtractor={(item) => item.type}
|
||||
contentContainerStyle={{ paddingBottom: 24 }}
|
||||
scrollEnabled={false}
|
||||
nestedScrollEnabled={false}
|
||||
/>
|
||||
</Animated.ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
@@ -267,10 +318,57 @@ const styles = StyleSheet.create({
|
||||
width: 22,
|
||||
height: 22,
|
||||
tintColor: Palette.white,
|
||||
marginRight: 12,
|
||||
},
|
||||
optionIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: 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,
|
||||
},
|
||||
rowActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
removeButton: {
|
||||
padding: 6,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
},
|
||||
removeIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
tintColor: Palette.white,
|
||||
},
|
||||
sectionTag: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.12)",
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionTagText: {
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
letterSpacing: 1,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -65,6 +65,10 @@ export const STRUCTURE_SEGMENTS = {
|
||||
"intro instrumentale longue",
|
||||
],
|
||||
},
|
||||
// pre_chorus: {
|
||||
// label: "Pré-refrain",
|
||||
// promptLabel: "Pré-refrain",
|
||||
// showIndex: false,
|
||||
pre_chorus: {
|
||||
label: "Pré-refrain",
|
||||
promptLabel: "Pré-refrain",
|
||||
@@ -81,6 +85,151 @@ export const STRUCTURE_SEGMENTS = {
|
||||
"pré chorus",
|
||||
],
|
||||
},
|
||||
pre_refrain_instrumental: {
|
||||
label: "Pré-refrain instrumental",
|
||||
promptLabel: "Pré-refrain instrumental",
|
||||
showIndex: false,
|
||||
allowMultiple: false,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
exclusiveGroup: "pre_chorus",
|
||||
requiresLyrics: false,
|
||||
aliases: ["instrumental_pre_chorus"],
|
||||
},
|
||||
pont: {
|
||||
label: "Pont",
|
||||
promptLabel: "Pont",
|
||||
showIndex: false,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 225,
|
||||
requiresLyrics: true,
|
||||
aliases: ["bridge"],
|
||||
},
|
||||
solo_de_guitare: {
|
||||
label: "Solo de guitare",
|
||||
promptLabel: "Solo de guitare",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: ["guitar_solo", "solo_guitare"],
|
||||
},
|
||||
solo_de_guitare_electrique: {
|
||||
label: "Solo de guitare électrique",
|
||||
promptLabel: "Solo de guitare électrique",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: ["electric_guitar_solo", "solo_guitare_electrique"],
|
||||
},
|
||||
solo_de_batterie: {
|
||||
label: "Solo de batterie",
|
||||
promptLabel: "Solo de batterie",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: ["drum_solo", "solo_batterie"],
|
||||
},
|
||||
solo_de_saxophone: {
|
||||
label: "Solo de saxophone",
|
||||
promptLabel: "Solo de saxophone",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: ["sax_solo", "solo_saxo"],
|
||||
},
|
||||
solo_de_violon: {
|
||||
label: "Solo de violon",
|
||||
promptLabel: "Solo de violon",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: ["violin_solo", "solo_violon"],
|
||||
},
|
||||
break: {
|
||||
label: "Break",
|
||||
promptLabel: "Break",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: [],
|
||||
},
|
||||
interlude: {
|
||||
label: "Interlude",
|
||||
promptLabel: "Interlude",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: [],
|
||||
},
|
||||
interlude_melodique: {
|
||||
label: "Interlude mélodique",
|
||||
promptLabel: "Interlude mélodique",
|
||||
showIndex: true,
|
||||
allowMultiple: true,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
requiresLyrics: false,
|
||||
aliases: ["melodic_interlude"],
|
||||
},
|
||||
final_apogee: {
|
||||
label: "Final apogée",
|
||||
promptLabel: "Final apogée",
|
||||
showIndex: false,
|
||||
allowMultiple: false,
|
||||
optional: true,
|
||||
inputHeight: 170,
|
||||
exclusiveGroup: "outro",
|
||||
requiresLyrics: false,
|
||||
aliases: ["grand_finale"],
|
||||
},
|
||||
arret_net: {
|
||||
label: "Arrêt net",
|
||||
promptLabel: "Arrêt net",
|
||||
showIndex: false,
|
||||
allowMultiple: false,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
exclusiveGroup: "outro",
|
||||
requiresLyrics: false,
|
||||
aliases: ["sudden_stop"],
|
||||
},
|
||||
fade_out: {
|
||||
label: "Fade out",
|
||||
promptLabel: "Fade out",
|
||||
showIndex: false,
|
||||
allowMultiple: false,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
exclusiveGroup: "outro",
|
||||
requiresLyrics: false,
|
||||
aliases: [],
|
||||
},
|
||||
transition_douce: {
|
||||
label: "Transition douce vers le silence",
|
||||
promptLabel: "Transition douce",
|
||||
showIndex: false,
|
||||
allowMultiple: false,
|
||||
optional: true,
|
||||
inputHeight: 150,
|
||||
exclusiveGroup: "outro",
|
||||
requiresLyrics: false,
|
||||
aliases: ["soft_transition"],
|
||||
},
|
||||
};
|
||||
|
||||
const ALIAS_TO_KEY = Object.entries(STRUCTURE_SEGMENTS).reduce(
|
||||
@@ -133,6 +282,7 @@ export const sanitizeStructureList = (structure = []) => {
|
||||
if (!Array.isArray(structure)) return [];
|
||||
const output = [];
|
||||
let shouldInjectPreChorus = false;
|
||||
let shouldInjectInstrumentalPreChorus = false;
|
||||
|
||||
structure.forEach((segment) => {
|
||||
const type = normalizeStructureType(segment);
|
||||
@@ -160,6 +310,10 @@ export const sanitizeStructureList = (structure = []) => {
|
||||
shouldInjectPreChorus = true;
|
||||
return;
|
||||
}
|
||||
if (type === "pre_refrain_instrumental") {
|
||||
shouldInjectInstrumentalPreChorus = true;
|
||||
return;
|
||||
}
|
||||
|
||||
output.push(type);
|
||||
});
|
||||
@@ -188,6 +342,38 @@ export const sanitizeStructureList = (structure = []) => {
|
||||
result = expanded;
|
||||
}
|
||||
|
||||
if (shouldInjectInstrumentalPreChorus) {
|
||||
const baseWithoutInstrumental = result.filter(
|
||||
(item) => item !== "pre_refrain_instrumental"
|
||||
);
|
||||
const expanded = [];
|
||||
|
||||
baseWithoutInstrumental.forEach((item) => {
|
||||
if (item === "pre_chorus") {
|
||||
const previous = expanded[expanded.length - 1];
|
||||
if (previous !== "pre_refrain_instrumental") {
|
||||
expanded.push("pre_refrain_instrumental");
|
||||
}
|
||||
}
|
||||
if (item === "refrain") {
|
||||
const previous = expanded[expanded.length - 1];
|
||||
if (
|
||||
previous !== "pre_refrain_instrumental" &&
|
||||
previous !== "pre_chorus"
|
||||
) {
|
||||
expanded.push("pre_refrain_instrumental");
|
||||
}
|
||||
}
|
||||
expanded.push(item);
|
||||
});
|
||||
|
||||
if (!expanded.includes("pre_refrain_instrumental")) {
|
||||
expanded.push("pre_refrain_instrumental");
|
||||
}
|
||||
|
||||
result = expanded;
|
||||
}
|
||||
|
||||
const introIndex = result.findIndex(
|
||||
(item) => item === "short_intro" || item === "long_intro"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user