diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js
index 1f1ecfb..07cd241 100644
--- a/functions/src/lyrics.js
+++ b/functions/src/lyrics.js
@@ -16,6 +16,20 @@ const STRUCTURE_PROMPT_LABELS = {
short_intro: "introduction instrumentale courte",
long_intro: "introduction instrumentale longue",
pre_chorus: "pré-refrain",
+ pre_refrain_instrumental: "pré-refrain instrumental",
+ pont: "pont",
+ solo_de_guitare: "solo de guitare",
+ solo_de_guitare_electrique: "solo de guitare électrique",
+ solo_de_batterie: "solo de batterie",
+ solo_de_saxophone: "solo de saxophone",
+ solo_de_violon: "solo de violon",
+ break: "break",
+ interlude: "interlude",
+ interlude_melodique: "interlude mélodique",
+ final_apogee: "final apogée",
+ arret_net: "arrêt net",
+ fade_out: "fade out",
+ transition_douce: "transition douce vers le silence",
};
const STRUCTURE_ALIASES = {
@@ -30,6 +44,20 @@ const STRUCTURE_ALIASES = {
"pre chorus": "pre_chorus",
"pre-chorus": "pre_chorus",
prechorus: "pre_chorus",
+ "pré-refrain instrumental": "pre_refrain_instrumental",
+ "pre-refrain instrumental": "pre_refrain_instrumental",
+ "instrumental pre-chorus": "pre_refrain_instrumental",
+ "instrumental pre chorus": "pre_refrain_instrumental",
+ bridge: "pont",
+ guitar_solo: "solo_de_guitare",
+ electric_guitar_solo: "solo_de_guitare_electrique",
+ drum_solo: "solo_de_batterie",
+ sax_solo: "solo_de_saxophone",
+ violin_solo: "solo_de_violon",
+ melodic_interlude: "interlude_melodique",
+ grand_finale: "final_apogee",
+ sudden_stop: "arret_net",
+ soft_transition: "transition_douce",
};
const normalizeStructureValue = (value) => {
@@ -61,6 +89,27 @@ const sanitizeStructureEntries = (structure = []) => {
if (!output.includes("pre_chorus")) output.push("pre_chorus");
return;
}
+ if (normalized === "pre_refrain_instrumental") {
+ if (!output.includes("pre_refrain_instrumental")) {
+ output.push("pre_refrain_instrumental");
+ }
+ return;
+ }
+ if (
+ normalized === "final_apogee" ||
+ normalized === "arret_net" ||
+ normalized === "fade_out" ||
+ normalized === "transition_douce"
+ ) {
+ const outroIndex = output.findIndex((item) =>
+ ["final_apogee", "arret_net", "fade_out", "transition_douce"].includes(
+ item,
+ ),
+ );
+ if (outroIndex !== -1) {
+ output.splice(outroIndex, 1);
+ }
+ }
output.push(normalized);
});
return output;
diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js
index bbaea96..a4f3250 100644
--- a/src/screens/Library/MusicDetails.js
+++ b/src/screens/Library/MusicDetails.js
@@ -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";
};
diff --git a/src/screens/Library/MusicDetails.web.js b/src/screens/Library/MusicDetails.web.js
index 82a4750..7ee8e38 100644
--- a/src/screens/Library/MusicDetails.web.js
+++ b/src/screens/Library/MusicDetails.web.js
@@ -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";
};
diff --git a/src/screens/Writing/CreatingLyrics.js b/src/screens/Writing/CreatingLyrics.js
index 34989fe..d5fbdc4 100644
--- a/src/screens/Writing/CreatingLyrics.js
+++ b/src/screens/Writing/CreatingLyrics.js
@@ -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"}
-
+
{
fontFamily: FONT_FAMILY.InterRegular,
}}
>
- {progress}%
+ {progressValue}%
{!saved && result && !error && (
diff --git a/src/screens/Writing/CustomizeSongStructure.js b/src/screens/Writing/CustomizeSongStructure.js
index a209367..c7a8481 100644
--- a/src/screens/Writing/CustomizeSongStructure.js
+++ b/src/screens/Writing/CustomizeSongStructure.js
@@ -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 (
{
{rowData?.label || ""}
{rowData?.sortable ? (
-
-
-
+
+
+
+
+ {canRemove && (
+ handleRemoveItem(rowData.id)}
+ style={styles.removeButton}
+ hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
+ >
+
+
+ )}
+
) : (
-
+ 0 ? styles.countBadgeActive : null,
+ ]}
+ >
+ {count}
+
)}
@@ -166,46 +213,50 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
contentContainerStyle={{ paddingVertical: 4, paddingBottom: 24 }}
showsVerticalScrollIndicator={false}
>
- <>
-
}
- customHandle
- scrollableRef={scrollRef}
- onDragEnd={({ data: newData }) => {
- const values = (newData || [])
- .map((it) => it?.type || it?.value)
- .filter(Boolean);
- const sanitized = sanitizeStructureList(values);
- setSortableItems(buildSortableItems(sanitized));
- }}
- />
- {
- const selected = selectedTypes.has(item.type);
- return (
-
- toggleOptionalSegment(item.type)}
- />
-
- );
- }}
- keyExtractor={(item) => item.type}
- contentContainerStyle={{ paddingBottom: 24 }}
- scrollEnabled={false}
- nestedScrollEnabled={false}
- />
- >
+
+ Structure actuelle
+
+
}
+ customHandle
+ scrollableRef={scrollRef}
+ onDragEnd={({ data: newData }) => {
+ const values = extractValues(newData);
+ const sanitized = sanitizeStructureList(values);
+ setSortableItems(buildSortableItems(sanitized));
+ }}
+ />
+
+ Éléments à ajouter
+
+ {
+ const count = segmentCounts[item.type] || 0;
+ const selected = count > 0;
+ return (
+
+ handleOptionalSegmentPress(item.type)}
+ />
+
+ );
+ }}
+ keyExtractor={(item) => item.type}
+ contentContainerStyle={{ paddingBottom: 24 }}
+ scrollEnabled={false}
+ nestedScrollEnabled={false}
+ />
@@ -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",
+ },
});
diff --git a/src/utils/songStructure.js b/src/utils/songStructure.js
index f2e266d..55439f8 100644
--- a/src/utils/songStructure.js
+++ b/src/utils/songStructure.js
@@ -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"
);