This commit is contained in:
2025-10-23 11:22:04 +02:00
parent 8024f71e1c
commit 6947195a9a
2 changed files with 190 additions and 189 deletions
+106 -125
View File
@@ -1,9 +1,7 @@
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { import {
FlatList, FlatList,
Image, Image,
Platform,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
@@ -21,6 +19,7 @@ import {
OPTIONAL_STRUCTURE_SEGMENTS, OPTIONAL_STRUCTURE_SEGMENTS,
sanitizeStructureList, sanitizeStructureList,
} from "../../utils/songStructure"; } from "../../utils/songStructure";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import CreateLyricsHeader from "./components/CreateLyricsHeader"; import CreateLyricsHeader from "./components/CreateLyricsHeader";
const randomSuffix = () => Math.random().toString(36).slice(2, 8); const randomSuffix = () => Math.random().toString(36).slice(2, 8);
@@ -138,66 +137,56 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
); );
const Row = ({ item: rowData, onPress }) => { 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 baseType = rowData?.type || rowData?.value;
const meta = getSegmentMeta(baseType); const meta = getSegmentMeta(baseType);
const canRemove = rowData?.sortable && meta?.optional === true; const canRemove = rowData?.sortable && meta?.optional === true;
const selected = rowData?.selected;
const count = rowData?.count ?? 0; const count = rowData?.count ?? 0;
const containerStyle = StyleSheet.flatten([
styles.rowContainer,
]);
const blurStyle = StyleSheet.flatten([
styles.rowBlur,
selected ? styles.selectedBlur : null,
]);
const badgeStyle = StyleSheet.flatten([
styles.countBadge,
count > 0 ? styles.countBadgeActive : null,
]);
return ( return (
<Wrapper <CreateLyricsHeader
tint="dark"
intensity={40}
showBorder={false}
onPress={onPress} onPress={onPress}
activeOpacity={0.8} containerStyle={containerStyle}
style={isButton ? styles.touchable : undefined} blurViewStyle={blurStyle}
> >
<View <View style={styles.rowContent}>
style={[ <Text style={styles.rowText}>{rowData?.label || ""}</Text>
styles.itemContainer, {rowData?.sortable ? (
selected && styles.selectedItem, <View style={styles.rowActions}>
rowData?.sortable && styles.sortableItem, <Sortable.Handle>
]} <Image source={icons.dragDots} style={styles.handleIcon} />
> </Sortable.Handle>
<BlurView {canRemove && (
intensity={Platform.OS !== "ios" ? 10 : 30} <TouchableOpacity
style={[ onPress={() => handleRemoveItem(rowData.id)}
styles.rowBlur, style={styles.removeButton}
selected && styles.selectedBlur, hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
isButton && styles.buttonBlur, >
]} <Image source={icons.close} style={styles.removeIcon} />
> </TouchableOpacity>
<View style={styles.rowContent}> )}
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
</View> </View>
{rowData?.sortable ? ( ) : (
<View style={styles.rowActions}> <View style={badgeStyle}>
<Sortable.Handle> <Text style={styles.countBadgeText}>{count}</Text>
<Image source={icons.dragDots} style={styles.handleIcon} /> </View>
</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>
) : (
<View
style={[
styles.countBadge,
count > 0 ? styles.countBadgeActive : null,
]}
>
<Text style={styles.countBadgeText}>{count}</Text>
</View>
)}
</BlurView>
</View> </View>
</Wrapper> </CreateLyricsHeader>
); );
}; };
@@ -216,47 +205,60 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
<View style={styles.sectionTag}> <View style={styles.sectionTag}>
<Text style={styles.sectionTagText}>Structure actuelle</Text> <Text style={styles.sectionTagText}>Structure actuelle</Text>
</View> </View>
<Sortable.Grid <ItemContainer
columns={1} height="auto"
rowGap={12} disableKeyboardHeight
data={labeledItems} style={styles.blurSection}
renderItem={({ item }) => <Row item={item} />} >
customHandle <Sortable.Grid
scrollableRef={scrollRef} columns={1}
onDragEnd={({ data: newData }) => { rowGap={12}
const values = extractValues(newData); data={labeledItems}
const sanitized = sanitizeStructureList(values); renderItem={({ item }) => <Row item={item} />}
setSortableItems(buildSortableItems(sanitized)); customHandle
}} scrollableRef={scrollRef}
/> onDragEnd={({ data: newData }) => {
const values = extractValues(newData);
const sanitized = sanitizeStructureList(values);
setSortableItems(buildSortableItems(sanitized));
}}
/>
</ItemContainer>
<View style={[styles.sectionTag, { marginTop: 24 }]}> <View style={[styles.sectionTag, { marginTop: 24 }]}>
<Text style={styles.sectionTagText}>Éléments à ajouter</Text> <Text style={styles.sectionTagText}>Éléments à ajouter</Text>
</View> </View>
<FlatList <ItemContainer
data={optionalSegments} height="auto"
gap={12} disableKeyboardHeight
renderItem={({ item }) => { style={styles.blurSection}
const count = segmentCounts[item.type] || 0; >
const selected = count > 0; <FlatList
return ( data={optionalSegments}
<View style={{ marginTop: 12 }}> gap={12}
<Row renderItem={({ item, index }) => {
item={{ const count = segmentCounts[item.type] || 0;
...item, const selected = count > 0;
sortable: false, const wrapperStyle = index === 0 ? null : styles.optionWrapper;
selected, return (
count, <View style={wrapperStyle}>
}} <Row
onPress={() => handleOptionalSegmentPress(item.type)} item={{
/> ...item,
</View> sortable: false,
); selected,
}} count,
keyExtractor={(item) => item.type} }}
contentContainerStyle={{ paddingBottom: 24 }} onPress={() => handleOptionalSegmentPress(item.type)}
scrollEnabled={false} />
nestedScrollEnabled={false} </View>
/> );
}}
keyExtractor={(item) => item.type}
contentContainerStyle={{ paddingBottom: 24 }}
scrollEnabled={false}
nestedScrollEnabled={false}
/>
</ItemContainer>
</Animated.ScrollView> </Animated.ScrollView>
</View> </View>
</View> </View>
@@ -266,40 +268,19 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
export default CustomizeSongStructure; export default CustomizeSongStructure;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
touchable: { rowContainer: {
borderRadius: 14,
width: "100%", width: "100%",
}, },
itemContainer: { blurSection: {
height: 54, marginTop: 12,
backgroundColor: Palette.glass, width: "100%",
borderRadius: 14,
overflow: "hidden",
shadowColor: "#00000040",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
sortableItem: {
marginBottom: 0,
},
selectedItem: {
borderWidth: 1,
borderColor: Palette.white,
}, },
rowBlur: { rowBlur: {
flex: 1, minHeight: 54,
backgroundColor: Palette.glass, paddingHorizontal: 16,
justifyContent: "center",
paddingHorizontal: 12,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
height: "100%",
},
buttonBlur: {
justifyContent: "space-between", justifyContent: "space-between",
}, },
selectedBlur: { selectedBlur: {
@@ -307,7 +288,9 @@ const styles = StyleSheet.create({
}, },
rowContent: { rowContent: {
flex: 1, flex: 1,
justifyContent: "center", flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}, },
rowText: { rowText: {
fontSize: 16, fontSize: 16,
@@ -320,11 +303,6 @@ const styles = StyleSheet.create({
tintColor: Palette.white, tintColor: Palette.white,
marginRight: 12, marginRight: 12,
}, },
optionIcon: {
width: 20,
height: 20,
tintColor: Palette.white,
},
countBadge: { countBadge: {
minWidth: 28, minWidth: 28,
paddingHorizontal: 8, paddingHorizontal: 8,
@@ -356,6 +334,9 @@ const styles = StyleSheet.create({
height: 16, height: 16,
tintColor: Palette.white, tintColor: Palette.white,
}, },
optionWrapper: {
marginTop: 12,
},
sectionTag: { sectionTag: {
alignSelf: "flex-start", alignSelf: "flex-start",
paddingHorizontal: 12, paddingHorizontal: 12,
@@ -23,6 +23,7 @@ import {
OPTIONAL_STRUCTURE_SEGMENTS, OPTIONAL_STRUCTURE_SEGMENTS,
sanitizeStructureList, sanitizeStructureList,
} from "../../utils/songStructure"; } from "../../utils/songStructure";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import CreateLyricsHeader from "./components/CreateLyricsHeader"; import CreateLyricsHeader from "./components/CreateLyricsHeader";
const ROW_HEIGHT = 54; const ROW_HEIGHT = 54;
@@ -222,12 +223,22 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const baseType = item?.type || item?.value; const baseType = item?.type || item?.value;
const meta = getSegmentMeta(baseType); const meta = getSegmentMeta(baseType);
const canRemove = meta?.optional === true; const canRemove = meta?.optional === true;
const containerStyle = StyleSheet.flatten([
styles.rowContainer,
activeItemId === item.id ? styles.activeItem : null,
]);
const blurStyle = StyleSheet.flatten([
styles.itemBlur,
activeItemId === item.id ? styles.activeBlur : null,
]);
return ( return (
<View <CreateLyricsHeader
style={[ tint="dark"
styles.itemContainer, intensity={40}
activeItemId === item.id && styles.activeItem, showBorder={false}
]} containerStyle={containerStyle}
blurViewStyle={blurStyle}
> >
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.rowText}>{item.label}</Text> <Text style={styles.rowText}>{item.label}</Text>
@@ -253,7 +264,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
</View> </View>
</View> </View>
</View> </View>
</View> </CreateLyricsHeader>
); );
}; };
@@ -271,38 +282,59 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
<View style={styles.sectionTag}> <View style={styles.sectionTag}>
<Text style={styles.sectionTagText}>Structure actuelle</Text> <Text style={styles.sectionTagText}>Structure actuelle</Text>
</View> </View>
<View style={styles.listContainer} ref={containerRef}> <ItemContainer
{labeledItems.map((item) => ( height="auto"
<StructureRow key={item.id} item={item} /> disableKeyboardHeight
))} style={styles.blurSection}
</View> >
<View style={styles.listContainer} ref={containerRef}>
{labeledItems.map((item) => (
<StructureRow key={item.id} item={item} />
))}
</View>
</ItemContainer>
<View style={[styles.sectionTag, styles.addSectionTag]}> <View style={[styles.sectionTag, styles.addSectionTag]}>
<Text style={styles.sectionTagText}>Éléments à ajouter</Text> <Text style={styles.sectionTagText}>Éléments à ajouter</Text>
</View> </View>
<View style={styles.optionsContainer}> <ItemContainer
{optionalSegments.map((segment) => { height="auto"
const count = segmentCounts[segment.type] || 0; disableKeyboardHeight
const selected = count > 0; style={styles.blurSection}
return ( >
<TouchableOpacity <View style={styles.optionsContainer}>
key={segment.type} {optionalSegments.map((segment) => {
activeOpacity={0.85} const count = segmentCounts[segment.type] || 0;
onPress={() => handleOptionalSegmentPress(segment.type)} const selected = count > 0;
style={[styles.optionItem, selected && styles.selectedItem]} const blurStyle = StyleSheet.flatten([
> styles.itemBlur,
<Text style={styles.rowText}>{segment.label}</Text> selected ? styles.selectedBlur : null,
<View ]);
style={[ const badgeStyle = StyleSheet.flatten([
styles.countBadge, styles.countBadge,
selected && styles.countBadgeActive, selected ? styles.countBadgeActive : null,
]} ]);
return (
<CreateLyricsHeader
key={segment.type}
tint="dark"
intensity={40}
showBorder={false}
onPress={() => handleOptionalSegmentPress(segment.type)}
containerStyle={styles.rowContainer}
blurViewStyle={blurStyle}
> >
<Text style={styles.countBadgeText}>{count}</Text> <View style={styles.row}>
</View> <Text style={styles.rowText}>{segment.label}</Text>
</TouchableOpacity> <View style={badgeStyle}>
); <Text style={styles.countBadgeText}>{count}</Text>
})} </View>
</View> </View>
</CreateLyricsHeader>
);
})}
</View>
</ItemContainer>
</ScrollView> </ScrollView>
</View> </View>
</View> </View>
@@ -337,24 +369,32 @@ const styles = StyleSheet.create({
listContainer: { listContainer: {
gap: 12, gap: 12,
}, },
itemContainer: { blurSection: {
height: ROW_HEIGHT, marginTop: 12,
width: "100%",
},
rowContainer: {
borderRadius: 14, borderRadius: 14,
backgroundColor: Palette.glass,
shadowColor: "#00000040",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
overflow: "hidden",
}, },
activeItem: { activeItem: {
borderWidth: 1, borderWidth: 1,
borderColor: Palette.white, borderColor: Palette.white,
}, },
itemBlur: {
minHeight: ROW_HEIGHT,
paddingHorizontal: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
activeBlur: {
backgroundColor: Palette.transparentWhite,
},
selectedBlur: {
backgroundColor: Palette.transparentWhite,
},
row: { row: {
flex: 1, flex: 1,
paddingHorizontal: 16,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
@@ -391,26 +431,6 @@ const styles = StyleSheet.create({
}, },
optionsContainer: { optionsContainer: {
gap: 12, gap: 12,
marginTop: 12,
},
optionItem: {
height: ROW_HEIGHT,
borderRadius: 14,
backgroundColor: Palette.glass,
shadowColor: "#00000040",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
overflow: "hidden",
paddingHorizontal: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
selectedItem: {
borderWidth: 1,
borderColor: Palette.white,
}, },
countBadge: { countBadge: {
minWidth: 28, minWidth: 28,