Files
musicland/src/screens/Writing/CustomizeSongStructure.js
T
2025-10-21 11:38:48 +02:00

288 lines
7.9 KiB
JavaScript

import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useState } from "react";
import {
FlatList,
Image,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import Animated, { useAnimatedRef } from "react-native-reanimated";
import Sortable from "react-native-sortables";
import { icons } from "../../assets";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import {
formatStructureLabel,
getSegmentMeta,
OPTIONAL_STRUCTURE_SEGMENTS,
sanitizeStructureList,
} from "../../utils/songStructure";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
const randomSuffix = () => Math.random().toString(36).slice(2, 8);
const createItemId = (type) => `${type}-${Date.now()}-${randomSuffix()}`;
const createSortableItem = (type, index = 0) => ({
id: `${type}-${index}-${randomSuffix()}`,
type,
value: type,
});
const buildInitialItems = (structure = []) =>
sanitizeStructureList(structure).map((type, index) =>
createSortableItem(type, index)
);
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const scrollRef = useAnimatedRef();
const sanitizedBase = useMemo(
() => sanitizeStructureList(baseStructure),
[baseStructure]
);
const [sortableItems, setSortableItems] = useState(() =>
buildInitialItems(sanitizedBase)
);
useEffect(() => {
setSortableItems((prev) => {
const prevTypes = prev.map((item) => item.type);
if (
prevTypes.length === sanitizedBase.length &&
prevTypes.every((type, index) => type === sanitizedBase[index])
) {
return prev;
}
return buildInitialItems(sanitizedBase);
});
}, [sanitizedBase]);
useEffect(() => {
const values = sortableItems.map((item) => item.type).filter(Boolean);
onChange?.(values);
}, [sortableItems, onChange]);
const labeledItems = useMemo(() => {
const counts = {};
return sortableItems.map((item) => {
const baseType = item.type || item.value;
const nextCount = (counts[baseType] || 0) + 1;
counts[baseType] = nextCount;
return {
...item,
type: baseType,
value: baseType,
label: formatStructureLabel(baseType, nextCount),
sortable: true,
};
});
}, [sortableItems]);
const optionalSegments = useMemo(
() =>
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
const meta = getSegmentMeta(type);
return { type, label: meta?.label || formatStructureLabel(type, 1) };
}),
[]
);
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);
});
return set;
}, [sortableItems]);
const toggleOptionalSegment = (type) => {
setSortableItems((prev) => {
const existsIndex = prev.findIndex((item) => item.type === type);
if (existsIndex !== -1) {
return prev.filter((item) => item.type !== type);
}
const meta = getSegmentMeta(type);
let next = [...prev];
if (meta?.exclusiveGroup) {
next = next.filter((item) => {
const itemMeta = getSegmentMeta(item.type || item.value);
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
});
}
next.push(createSortableItem(type, next.length));
return next;
});
};
const Row = ({ item: rowData, onPress }) => {
const isButton = typeof onPress === "function";
const Wrapper = isButton ? TouchableOpacity : View;
const selected = rowData?.selected;
const icon = rowData?.icon;
return (
<Wrapper
onPress={onPress}
activeOpacity={0.8}
style={isButton ? styles.touchable : undefined}
>
<View
style={[
styles.itemContainer,
selected && styles.selectedItem,
rowData?.sortable && styles.sortableItem,
]}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 30}
style={[
styles.rowBlur,
selected && styles.selectedBlur,
isButton && styles.buttonBlur,
]}
>
<View style={styles.rowContent}>
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
</View>
{rowData?.sortable ? (
<Sortable.Handle>
<Image source={icons.dragDots} style={styles.handleIcon} />
</Sortable.Handle>
) : (
<Image
source={icon || (selected ? icons.check : icons.add)}
style={[styles.optionIcon]}
/>
)}
</BlurView>
</View>
</Wrapper>
);
};
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title="Personnalise la structure de ta chanson"
subTitle="Réorganise par glisser-déposer"
/>
<View style={{ flex: 1 }}>
<Animated.ScrollView
ref={scrollRef}
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 next = (newData || []).map((it, index) => {
const nextType = it?.type || it?.value;
return {
id: it?.id || createItemId(nextType || `section-${index}`),
type: nextType,
value: nextType,
};
});
setSortableItems(next.filter((item) => item.type));
}}
/>
<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}
/>
</>
</Animated.ScrollView>
</View>
</View>
);
};
export default CustomizeSongStructure;
const styles = StyleSheet.create({
touchable: {
width: "100%",
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
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: {
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
height: "100%",
},
buttonBlur: {
justifyContent: "space-between",
},
selectedBlur: {
backgroundColor: Palette.transparentWhite,
},
rowContent: {
flex: 1,
justifyContent: "center",
},
rowText: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
handleIcon: {
width: 22,
height: 22,
tintColor: Palette.white,
},
optionIcon: {
width: 20,
height: 20,
tintColor: Palette.white,
},
});