288 lines
7.9 KiB
JavaScript
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,
|
|
},
|
|
});
|