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" );