separate structure and option
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
arraysAreSame,
|
||||
formatStructureLabel,
|
||||
getSegmentMeta,
|
||||
OPTIONAL_STRUCTURE_SEGMENTS,
|
||||
sanitizeStructureList,
|
||||
} from '../../utils/songStructure'
|
||||
import ItemContainer from '../../components/ItemContainer/ItemContainer'
|
||||
@@ -28,6 +27,32 @@ 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 AUTO_STRUCTURE_SEGMENTS = [
|
||||
'short_intro',
|
||||
'long_intro',
|
||||
PRE_REFRAIN_TYPE,
|
||||
'final_apogee',
|
||||
'arret_net',
|
||||
'fade_out',
|
||||
'transition_douce',
|
||||
]
|
||||
const DRAG_STRUCTURE_SEGMENTS = [
|
||||
'solo_de_guitare',
|
||||
'solo_de_guitare_electrique',
|
||||
'solo_de_batterie',
|
||||
'solo_de_saxophone',
|
||||
'solo_de_violon',
|
||||
'break',
|
||||
'pont',
|
||||
]
|
||||
|
||||
const isAutoSegmentType = (type) => {
|
||||
if (!type) return false
|
||||
if (INTRO_TYPES.has(type)) return true
|
||||
if (type === PRE_REFRAIN_TYPE) return true
|
||||
const meta = getSegmentMeta(type)
|
||||
return meta?.exclusiveGroup === 'outro'
|
||||
}
|
||||
|
||||
const buildSortableItems = (values = []) =>
|
||||
sanitizeStructureList(values, SANITIZE_OPTIONS).map((type, index) =>
|
||||
@@ -106,14 +131,24 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
type: baseType,
|
||||
value: baseType,
|
||||
label: formatStructureLabel(baseType, nextCount),
|
||||
sortable: true,
|
||||
mode: 'structure',
|
||||
draggable: !isAutoSegmentType(baseType),
|
||||
}
|
||||
})
|
||||
}, [sortableItems])
|
||||
|
||||
const optionalSegments = useMemo(
|
||||
const autoSegments = useMemo(
|
||||
() =>
|
||||
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
||||
AUTO_STRUCTURE_SEGMENTS.map((type) => {
|
||||
const meta = getSegmentMeta(type)
|
||||
return { type, label: meta?.label || formatStructureLabel(type, 1) }
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const dragSegments = useMemo(
|
||||
() =>
|
||||
DRAG_STRUCTURE_SEGMENTS.map((type) => {
|
||||
const meta = getSegmentMeta(type)
|
||||
return { type, label: meta?.label || formatStructureLabel(type, 1) }
|
||||
}),
|
||||
@@ -191,7 +226,10 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
const Row = ({ item: rowData, onPress }) => {
|
||||
const baseType = rowData?.type || rowData?.value
|
||||
const meta = getSegmentMeta(baseType)
|
||||
const canRemove = rowData?.sortable && meta?.optional === true
|
||||
const isOptionRow = rowData?.mode === 'option'
|
||||
const showActions = !isOptionRow
|
||||
const canRemove = showActions && meta?.optional === true
|
||||
const canDrag = showActions && rowData?.draggable !== false
|
||||
const selected = rowData?.selected
|
||||
const count = rowData?.count ?? 0
|
||||
const containerStyle = StyleSheet.flatten([styles.rowContainer])
|
||||
@@ -212,11 +250,13 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
>
|
||||
<View style={styles.rowContent}>
|
||||
<Text style={styles.rowText}>{rowData?.label || ''}</Text>
|
||||
{rowData?.sortable ? (
|
||||
{showActions ? (
|
||||
<View style={styles.rowActions}>
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
{canDrag && (
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
)}
|
||||
{canRemove && (
|
||||
<TouchableOpacity
|
||||
onPress={() => handleRemoveItem(rowData.id)}
|
||||
@@ -241,7 +281,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
|
||||
<CreateLyricsHeader
|
||||
title="Personnalise la structure de ta chanson"
|
||||
subTitle="Réorganise par glisser-déposer"
|
||||
subTitle="Active les options automatiques et réorganise par glisser-déposer"
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Animated.ScrollView
|
||||
@@ -274,11 +314,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
/>
|
||||
</ItemContainer>
|
||||
<View style={[styles.sectionTag, { marginTop: 24 }]}>
|
||||
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
||||
<Text style={styles.sectionTagText}>Options automatiques</Text>
|
||||
</View>
|
||||
<ItemContainer height="auto" disableKeyboardHeight style={styles.blurSection}>
|
||||
<FlatList
|
||||
data={optionalSegments}
|
||||
data={autoSegments}
|
||||
gap={12}
|
||||
renderItem={({ item, index }) => {
|
||||
const count = segmentCounts[item.type] || 0
|
||||
@@ -289,7 +329,38 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
<Row
|
||||
item={{
|
||||
...item,
|
||||
sortable: false,
|
||||
mode: 'option',
|
||||
selected,
|
||||
count,
|
||||
}}
|
||||
onPress={() => handleOptionalSegmentPress(item.type)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}}
|
||||
keyExtractor={(item) => item.type}
|
||||
contentContainerStyle={{ paddingBottom: 24 }}
|
||||
scrollEnabled={false}
|
||||
nestedScrollEnabled={false}
|
||||
/>
|
||||
</ItemContainer>
|
||||
<View style={[styles.sectionTag, { marginTop: 24 }]}>
|
||||
<Text style={styles.sectionTagText}>Options glisser-déposer</Text>
|
||||
</View>
|
||||
<ItemContainer height="auto" disableKeyboardHeight style={styles.blurSection}>
|
||||
<FlatList
|
||||
data={dragSegments}
|
||||
gap={12}
|
||||
renderItem={({ item, index }) => {
|
||||
const count = segmentCounts[item.type] || 0
|
||||
const selected = count > 0
|
||||
const wrapperStyle = index === 0 ? null : styles.optionWrapper
|
||||
return (
|
||||
<View style={wrapperStyle}>
|
||||
<Row
|
||||
item={{
|
||||
...item,
|
||||
mode: 'option',
|
||||
selected,
|
||||
count,
|
||||
}}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
arraysAreSame,
|
||||
formatStructureLabel,
|
||||
getSegmentMeta,
|
||||
OPTIONAL_STRUCTURE_SEGMENTS,
|
||||
sanitizeStructureList,
|
||||
} from '../../utils/songStructure'
|
||||
import CreateLyricsHeader from './components/CreateLyricsHeader'
|
||||
@@ -29,6 +28,24 @@ 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 AUTO_STRUCTURE_SEGMENTS = [
|
||||
'short_intro',
|
||||
'long_intro',
|
||||
PRE_REFRAIN_TYPE,
|
||||
'final_apogee',
|
||||
'arret_net',
|
||||
'fade_out',
|
||||
'transition_douce',
|
||||
]
|
||||
const DRAG_STRUCTURE_SEGMENTS = [
|
||||
'solo_de_guitare',
|
||||
'solo_de_guitare_electrique',
|
||||
'solo_de_batterie',
|
||||
'solo_de_saxophone',
|
||||
'solo_de_violon',
|
||||
'break',
|
||||
'pont',
|
||||
]
|
||||
|
||||
const PALETTE_SEGMENT_OVERRIDES = [
|
||||
{
|
||||
@@ -36,6 +53,18 @@ const PALETTE_SEGMENT_OVERRIDES = [
|
||||
description:
|
||||
'Transition contrastée qui relance l’énergie avant d’entrer dans la dernière partie.',
|
||||
},
|
||||
{
|
||||
type: 'short_intro',
|
||||
description: 'Introduction courte placée en ouverture du morceau.',
|
||||
},
|
||||
{
|
||||
type: 'long_intro',
|
||||
description: 'Introduction longue pour installer l’ambiance dès le début.',
|
||||
},
|
||||
{
|
||||
type: 'pre_refrain_instrumental',
|
||||
description: 'Se place automatiquement juste avant un refrain.',
|
||||
},
|
||||
{
|
||||
type: 'interlude_melodique',
|
||||
description: 'Respiration instrumentale qui met la mélodie à l’honneur sans paroles.',
|
||||
@@ -44,10 +73,18 @@ const PALETTE_SEGMENT_OVERRIDES = [
|
||||
type: 'final_apogee',
|
||||
description: 'Clôture apothéose qui amplifie l’émotion pour un final impactant.',
|
||||
},
|
||||
{
|
||||
type: 'arret_net',
|
||||
description: 'Final abrupt qui coupe net la musique.',
|
||||
},
|
||||
{
|
||||
type: 'fade_out',
|
||||
description: 'Sortie progressive où le morceau disparaît doucement dans le mix.',
|
||||
},
|
||||
{
|
||||
type: 'transition_douce',
|
||||
description: 'Fin en douceur qui glisse vers le silence.',
|
||||
},
|
||||
]
|
||||
|
||||
const getPaletteSegmentInfo = (type) => {
|
||||
@@ -59,6 +96,14 @@ const getPaletteSegmentInfo = (type) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isAutoSegmentType = (type) => {
|
||||
if (!type) return false
|
||||
if (INTRO_TYPES.has(type)) return true
|
||||
if (type === PRE_REFRAIN_TYPE) return true
|
||||
const meta = getSegmentMeta(type)
|
||||
return meta?.exclusiveGroup === 'outro'
|
||||
}
|
||||
|
||||
const getScrollY = () => (typeof window !== 'undefined' ? window.scrollY : 0)
|
||||
|
||||
const randomSuffix = () => Math.random().toString(36).slice(2, 8)
|
||||
@@ -241,14 +286,29 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
[sortableItems]
|
||||
)
|
||||
|
||||
const paletteSegments = useMemo(
|
||||
const autoPaletteSegments = useMemo(
|
||||
() =>
|
||||
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
||||
AUTO_STRUCTURE_SEGMENTS.map((type) => {
|
||||
const info = getPaletteSegmentInfo(type)
|
||||
return {
|
||||
type,
|
||||
label: info.label,
|
||||
description: info.description,
|
||||
placement: 'auto',
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const dragPaletteSegments = useMemo(
|
||||
() =>
|
||||
DRAG_STRUCTURE_SEGMENTS.map((type) => {
|
||||
const info = getPaletteSegmentInfo(type)
|
||||
return {
|
||||
type,
|
||||
label: info.label,
|
||||
description: info.description,
|
||||
placement: 'drag',
|
||||
}
|
||||
}),
|
||||
[]
|
||||
@@ -756,11 +816,79 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
[attachGlobalListeners]
|
||||
)
|
||||
|
||||
const renderPaletteSegment = (segment) => {
|
||||
const count = segmentCounts[segment.type] || 0
|
||||
const isActive = activeItemId === `palette-${segment.type}`
|
||||
const isDraggable = segment.placement === 'drag'
|
||||
const description =
|
||||
segment.description || (segment.placement === 'auto' ? 'Placement automatique.' : '')
|
||||
const blurStyle = StyleSheet.flatten([
|
||||
styles.itemBlur,
|
||||
styles.paletteItemBlur,
|
||||
isActive ? styles.activeBlur : null,
|
||||
])
|
||||
const badgeStyle = StyleSheet.flatten([
|
||||
styles.countBadge,
|
||||
count > 0 ? styles.countBadgeActive : null,
|
||||
])
|
||||
|
||||
return (
|
||||
<View
|
||||
key={segment.type}
|
||||
style={StyleSheet.flatten([
|
||||
styles.paletteCardWrapper,
|
||||
!isDraggable ? styles.paletteCardWrapperAuto : null,
|
||||
])}
|
||||
onPointerDown={isDraggable ? (event) => startPaletteDrag(segment.type, event) : undefined}
|
||||
>
|
||||
<CreateLyricsHeader
|
||||
tint="dark"
|
||||
intensity={40}
|
||||
showBorder={false}
|
||||
containerStyle={styles.rowContainer}
|
||||
blurViewStyle={blurStyle}
|
||||
>
|
||||
<View style={styles.paletteRow}>
|
||||
<View style={styles.paletteTextWrapper}>
|
||||
<Text style={styles.rowText}>{segment.label}</Text>
|
||||
{description ? (
|
||||
<Text style={styles.paletteDescription}>{description}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.paletteActions}>
|
||||
<Pressable
|
||||
onPointerDown={(ev) => ev?.stopPropagation?.()}
|
||||
onPressIn={(ev) => ev?.stopPropagation?.()}
|
||||
onPress={(ev) => {
|
||||
ev?.stopPropagation?.()
|
||||
const result = addOptionalSegment(segment.type)
|
||||
if (result?.success) {
|
||||
setInsertPreviewIndex(null)
|
||||
setActiveItemId(null)
|
||||
}
|
||||
}}
|
||||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||
style={({ pressed }) =>
|
||||
StyleSheet.flatten([styles.addButton, pressed ? styles.addButtonPressed : null])
|
||||
}
|
||||
>
|
||||
<Image source={icons.add} style={styles.addIcon} />
|
||||
</Pressable>
|
||||
<View style={badgeStyle}>
|
||||
<Text style={styles.countBadgeText}>{count}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const StructureRow = ({ item }) => {
|
||||
const baseType = item?.type || item?.value
|
||||
const meta = getSegmentMeta(baseType)
|
||||
const canRemove = meta?.optional === true
|
||||
const isIntroduction = INTRO_TYPES.has(baseType)
|
||||
const isAutoSegment = isAutoSegmentType(baseType)
|
||||
const rowRef = useRef(null)
|
||||
const isActive = activeItemId === item.id
|
||||
const containerStyle = StyleSheet.flatten([
|
||||
@@ -775,7 +903,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
])
|
||||
const rowStyle = StyleSheet.flatten([
|
||||
styles.row,
|
||||
isIntroduction ? styles.rowDisabled : styles.rowDraggable,
|
||||
isAutoSegment ? styles.rowDisabled : styles.rowDraggable,
|
||||
isActive ? styles.rowHidden : null,
|
||||
])
|
||||
|
||||
@@ -791,7 +919,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
ref={rowRef}
|
||||
style={rowStyle}
|
||||
onPointerDown={(event) => {
|
||||
if (isIntroduction) return
|
||||
if (isAutoSegment) return
|
||||
startItemDrag(item.id, event, rowRef.current)
|
||||
}}
|
||||
>
|
||||
@@ -811,7 +939,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
<Image source={icons.close} style={styles.removeIcon} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!isIntroduction && (
|
||||
{!isAutoSegment && (
|
||||
<View
|
||||
style={styles.handleWrapper}
|
||||
onPointerDown={(event) => {
|
||||
@@ -832,7 +960,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
<View style={styles.container}>
|
||||
<CreateLyricsHeader
|
||||
title="Personnalise la structure de ta chanson"
|
||||
subTitle="Réorganise par glisser-déposer"
|
||||
subTitle="Active les options automatiques et réorganise par glisser-déposer"
|
||||
/>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.columns}>
|
||||
@@ -865,9 +993,6 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
</ItemContainer>
|
||||
</View>
|
||||
<View style={[styles.column, styles.paletteColumn]}>
|
||||
<View style={[styles.sectionTag, styles.addSectionTag]}>
|
||||
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
||||
</View>
|
||||
<ItemContainer
|
||||
height="auto"
|
||||
disableKeyboardHeight
|
||||
@@ -878,71 +1003,21 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator
|
||||
>
|
||||
<View style={styles.paletteList}>
|
||||
{paletteSegments.map((segment) => {
|
||||
const count = segmentCounts[segment.type] || 0
|
||||
const isActive = activeItemId === `palette-${segment.type}`
|
||||
const blurStyle = StyleSheet.flatten([
|
||||
styles.itemBlur,
|
||||
styles.paletteItemBlur,
|
||||
isActive ? styles.activeBlur : null,
|
||||
])
|
||||
const badgeStyle = StyleSheet.flatten([
|
||||
styles.countBadge,
|
||||
count > 0 ? styles.countBadgeActive : null,
|
||||
])
|
||||
|
||||
return (
|
||||
<View
|
||||
key={segment.type}
|
||||
style={styles.paletteCardWrapper}
|
||||
onPointerDown={(event) => startPaletteDrag(segment.type, event)}
|
||||
>
|
||||
<CreateLyricsHeader
|
||||
tint="dark"
|
||||
intensity={40}
|
||||
showBorder={false}
|
||||
containerStyle={styles.rowContainer}
|
||||
blurViewStyle={blurStyle}
|
||||
>
|
||||
<View style={styles.paletteRow}>
|
||||
<View style={styles.paletteTextWrapper}>
|
||||
<Text style={styles.rowText}>{segment.label}</Text>
|
||||
{segment.description ? (
|
||||
<Text style={styles.paletteDescription}>{segment.description}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.paletteActions}>
|
||||
<Pressable
|
||||
onPointerDown={(ev) => ev?.stopPropagation?.()}
|
||||
onPressIn={(ev) => ev?.stopPropagation?.()}
|
||||
onPress={(ev) => {
|
||||
ev?.stopPropagation?.()
|
||||
const result = addOptionalSegment(segment.type)
|
||||
if (result?.success) {
|
||||
setInsertPreviewIndex(null)
|
||||
setActiveItemId(null)
|
||||
}
|
||||
}}
|
||||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||
style={({ pressed }) =>
|
||||
StyleSheet.flatten([
|
||||
styles.addButton,
|
||||
pressed ? styles.addButtonPressed : null,
|
||||
])
|
||||
}
|
||||
>
|
||||
<Image source={icons.add} style={styles.addIcon} />
|
||||
</Pressable>
|
||||
<View style={badgeStyle}>
|
||||
<Text style={styles.countBadgeText}>{count}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
<View style={styles.paletteSection}>
|
||||
<View style={styles.sectionTag}>
|
||||
<Text style={styles.sectionTagText}>Options automatiques</Text>
|
||||
</View>
|
||||
<View style={styles.paletteList}>
|
||||
{autoPaletteSegments.map((segment) => renderPaletteSegment(segment))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.paletteSection}>
|
||||
<View style={styles.sectionTag}>
|
||||
<Text style={styles.sectionTagText}>Options glisser-déposer</Text>
|
||||
</View>
|
||||
<View style={styles.paletteList}>
|
||||
{dragPaletteSegments.map((segment) => renderPaletteSegment(segment))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ItemContainer>
|
||||
@@ -1033,9 +1108,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 999,
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
addSectionTag: {
|
||||
marginTop: 0,
|
||||
},
|
||||
sectionTagText: {
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
@@ -1145,9 +1217,16 @@ const styles = StyleSheet.create({
|
||||
paletteList: {
|
||||
gap: 12,
|
||||
},
|
||||
paletteSection: {
|
||||
gap: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
paletteCardWrapper: {
|
||||
cursor: 'grab',
|
||||
},
|
||||
paletteCardWrapperAuto: {
|
||||
cursor: 'default',
|
||||
},
|
||||
paletteRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
|
||||
Reference in New Issue
Block a user