Change main page and modify flows

This commit is contained in:
Thomas Demirdjian
2025-09-02 11:45:27 +02:00
parent 8b7980308a
commit 98367af0b7
47 changed files with 3695 additions and 1020 deletions
+17 -5
View File
@@ -1,4 +1,5 @@
import { View, Text, Image, Platform, Pressable } from "react-native";
import { View, Text, Platform, Pressable, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React from "react";
import { img } from "../../../assets";
import { BlurView } from "expo-blur";
@@ -16,10 +17,21 @@ const SongCard = ({ rank = 1, title = "Sans titre", artist = "MusicLand", coverU
gap: 6,
}}
>
<Image
source={coverUrl ? { uri: coverUrl } : img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
{coverUrl ? (
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
)}
<BlurView
intensity={20}
style={{
+120 -118
View File
@@ -1,53 +1,32 @@
import {
View,
Text,
Image,
ScrollView,
Pressable,
Platform,
} from "react-native";
import React from "react";
import { View, Text, Image, Platform } from "react-native";
import React, { useMemo, useState } from "react";
import Page from "../layouts/Page";
import { ai, background, img } from "../assets";
import { background, img } from "../assets";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { BlurView } from "expo-blur";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
const CREATE_DATA = [
{
img: ai.nathalie,
bg: background.writingBG,
label: "Nathalie",
desc: "Lets write lyrics together !",
type: "Songwriter",
},
{
img: ai.theo,
bg: background.studioBG,
label: "Theo",
desc: "Come back, when\nyoull have lyrics!",
type: "Beatmaker",
},
{
img: ai.bena,
bg: background.productionBG,
label: "Bena",
desc: "Come back when you want\nto publish the song!",
type: "Producer",
},
{
img: ai.john,
bg: background.playbackBG,
label: "John",
desc: "Come back when your\naudio is ready!",
type: "Director",
},
];
import { FlatList, Alert } from "react-native";
import { BlurView } from "expo-blur";
import { useUser } from "../providers/UserDataProvider";
import MusicCard from "./Library/components/MusicCard";
import GradientButton from "../components/GradientButton";
import MoreMenu from "../components/MoreMenu";
import { projectsRef } from "../config/firebase";
import { useGlobal } from "reactn";
const Home = () => {
const { userProjects = [] } = useUser();
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []),
[userProjects],
);
const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
return (
<Page backgroundImg={background.homeBG} headerType="NONE">
<View style={{ flex: 1 }}>
@@ -55,89 +34,112 @@ const Home = () => {
source={img.goodVibe}
style={{ alignSelf: "center", position: "absolute" }}
/>
<ScrollView
contentContainerStyle={{
flexGrow: 1,
gap: 57,
paddingBottom: responsiveHeight(20),
}}
showsVerticalScrollIndicator={false}
>
<View style={{ height: responsiveHeight(60) }} />
{CREATE_DATA.map((item, index) => (
<Pressable
key={index}
onPress={() => {
switch (index) {
case 0:
navigate(Routes.Create, {
action: item.type,
});
break;
case 1:
navigate(Routes.Studio, {
action: item.type,
});
break;
}
{projects.length > 0 && (
<View
style={{
height: responsiveHeight(70),
paddingTop: responsiveHeight(6),
}}
>
<BlurView
intensity={20}
style={{
flex: 1,
borderRadius: 20,
overflow: "hidden",
backgroundColor: Palette.glass,
padding: 12,
gap: 8,
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<BlurView
tint="dark"
intensity={20}
<Text
style={{
position: "absolute",
zIndex: 1,
left: 110,
top: 10,
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 14,
borderBottomLeftRadius: 0,
overflow: "hidden",
backgroundColor: Palette.glass,
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
marginBottom: 8,
textAlign: "center",
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<Text
style={{
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{item.label}, {item.type}
</Text>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item.desc}
</Text>
</BlurView>
<Image
source={item.bg}
style={{
width: "100%",
height: 152,
borderRadius: 20,
position: "absolute",
bottom: 0,
}}
Musiques en cours
</Text>
<FlatList
data={projects}
keyExtractor={(item) => item.id}
contentContainerStyle={{ gap: 10, paddingBottom: 10 }}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={"MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() =>
navigate(Routes.FlowSelection, { projectId: item.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
/>
)}
/>
<Image
source={item.img}
style={{ width: 130, height: 200 }}
resizeMode="contain"
<MoreMenu
visible={showMenu}
top={menuTop}
onClose={() => setShowMenu(false)}
inPlaylist={false}
projectId={selectedProjectId}
extraItems={[
{
label: "Supprimer",
onPress: () =>
Alert.alert(
"Confirmer la suppression",
"Cette action supprimera définitivement ce projet.",
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
if (!selectedProjectId) return;
await projectsRef
.doc(selectedProjectId)
.delete();
setTooltip({
type: "success",
text: "Projet supprimé",
});
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
}
},
},
],
{ cancelable: true },
),
},
]}
/>
</Pressable>
))}
</ScrollView>
</BlurView>
</View>
)}
<View style={{ paddingTop: 12, marginBottom: responsiveHeight(10) }}>
<GradientButton
title="Créer une nouvelle musique"
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => navigate(Routes.FlowSelection)}
/>
</View>
</View>
</Page>
);
+17 -7
View File
@@ -1,4 +1,5 @@
import { View, Text, FlatList, Image, Pressable } from "react-native";
import { View, Text, FlatList, Pressable, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React, { useState } from "react";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
@@ -56,12 +57,21 @@ const AllMyMusic = () => {
navigate(Routes.MusicDetails, { projectId: item.id })
}
>
<Image
source={
item?.coverUrl ? { uri: item.coverUrl } : img.placeholder2
}
style={{ width: "100%", height: 114, borderRadius: 8 }}
/>
{item?.coverUrl ? (
<ExpoImage
source={{ uri: item.coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ width: "100%", height: 114, borderRadius: 8 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ width: "100%", height: 114, borderRadius: 8 }}
/>
)}
<Text
style={{
fontSize: 12,
+1
View File
@@ -23,6 +23,7 @@ const Library = () => {
<SearchBar
textInputProps={{
editable: false,
onPress: () => navigate(Routes.Research),
}}
/>
</Pressable>
+32 -30
View File
@@ -1,14 +1,8 @@
import { useRoute } from "@react-navigation/core";
import { Audio } from "expo-av";
import { Audio } from "expo-audio";
import React, { useEffect, useMemo, useState } from "react";
import {
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { Pressable, ScrollView, StyleSheet, Text, View, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { SheetManager } from "react-native-actions-sheet";
import { useGlobal } from "reactn";
import { background, icons, img } from "../../assets";
@@ -78,7 +72,7 @@ const MusicDetails = () => {
const soundRef = React.useRef(null);
// Load/unload audio with expo-av for reliable status updates on iOS/Android
// Load/unload audio with expo-audio for reliable status updates on iOS/Android
useEffect(() => {
let isMounted = true;
const load = async () => {
@@ -95,16 +89,16 @@ const MusicDetails = () => {
if (!songUrl) return;
const { sound } = await Audio.Sound.createAsync(
{ uri: songUrl },
{ shouldPlay: false },
(status) => {
if (!isMounted) return;
if (!status || !status.isLoaded) return;
const pos = status.positionMillis || 0;
const dur = status.durationMillis || 0;
setProgressInfo({ pos, dur });
setIsPlaying(!!status.isPlaying);
}
{ shouldPlay: false }
);
sound.setOnPlaybackStatusUpdate((status) => {
if (!isMounted) return;
if (!status || !status.isLoaded) return;
const pos = status.positionMillis || 0;
const dur = status.durationMillis || 0;
setProgressInfo({ pos, dur });
setIsPlaying(!!status.isPlaying);
});
soundRef.current = sound;
} catch (e) {
console.log("Audio load error", e?.message);
@@ -236,7 +230,7 @@ const MusicDetails = () => {
},
rightComponent: () => (
<Pressable onPress={() => SheetManager.show("DeleteAudio")}>
<Image source={icons.trash} />
<RNImage source={icons.trash} />
</Pressable>
),
})}
@@ -250,10 +244,18 @@ const MusicDetails = () => {
// stickyHeaderIndices={[1]}
>
<View style={{ gap: 28 }}>
<Image
source={coverUrl ? { uri: coverUrl } : img.placeholder4}
style={styles.img}
/>
{coverUrl ? (
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={150}
style={styles.img}
/>
) : (
<RNImage source={img.placeholder4} style={styles.img} />
)}
<View style={{ ...Style.containerSpaceBetween }}>
<View>
<Text style={styles.title}>{title}</Text>
@@ -276,7 +278,7 @@ const MusicDetails = () => {
}
}}
>
<Image
<RNImage
source={fav ? icons.heart : icons.heartOutline}
style={{ ...size({ size: 24 }) }}
resizeMode="contain"
@@ -287,7 +289,7 @@ const MusicDetails = () => {
SheetManager.show("Playlist", { payload: { projectId } })
}
>
<Image
<RNImage
source={icons.more}
style={{ ...size({ size: 24 }) }}
resizeMode="contain"
@@ -334,8 +336,8 @@ const MusicDetails = () => {
/>
<View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}>
{/* Previous (rewind 10s) */}
<Pressable onPress={() => seekBy(-10)}>
<Image
<Pressable onPress={() => seekBy(-10)}>
<RNImage
source={icons.forward}
style={{
...size({ size: 30 }),
@@ -351,7 +353,7 @@ const MusicDetails = () => {
}}
onPress={togglePlay}
>
<Image
<RNImage
resizeMode={"contain"}
source={isPlaying ? icons.pause : icons.play}
style={size({ size: 34 })}
@@ -359,7 +361,7 @@ const MusicDetails = () => {
</Pressable>
{/* Next (forward 10s) */}
<Pressable onPress={() => seekBy(10)}>
<Image
<RNImage
source={icons.forward}
style={{
...size({ size: 30 }),
+37 -3
View File
@@ -1,7 +1,8 @@
import React, { useState } from "react";
import { View } from "react-native";
import { View, Pressable, Image, Alert } from "react-native";
import { useGlobal } from "reactn";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { background, icons } from "../../assets";
import { gutters } from "../../styles";
import { useRoute } from "@react-navigation/native";
import { useDataFromRef } from "react-native-minuit/src/hooks";
@@ -9,10 +10,11 @@ import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId
import { playlistsRef, projectsRef } from "../../config/firebase";
import MusicCard from "./components/MusicCard";
import MoreMenu from "../../components/MoreMenu";
import { navigate } from "../../navigation/NavigationService";
import { navigate, goBack } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const PlaylistDetails = () => {
const [, setTooltip] = useGlobal("_tooltip");
const route = useRoute();
const playlistId = route?.params?.playlistId;
@@ -37,11 +39,43 @@ const PlaylistDetails = () => {
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const confirmDelete = () => {
Alert.alert(
"Supprimer la playlist",
"Es-tu sûr de vouloir supprimer cette playlist ?",
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
if (playlistId) {
await playlistsRef.doc(playlistId).delete();
setTooltip({ type: "success", text: "Playlist supprimée" });
}
} catch (e) {
console.log("Delete playlist error", e?.message);
setTooltip({ type: "error", text: e?.message || "Suppression impossible" });
} finally {
goBack();
}
},
},
],
);
};
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.libraryBG}
title={playlist?.name || "Playlist"}
rightComponent={() => (
<Pressable onPress={confirmDelete}>
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
</Pressable>
)}
contentContainerStyle={{ paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, gap: 10 }}>
+259 -148
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from "react";
import { Image, Pressable, ScrollView, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { background, img } from "../../assets";
import MoreMenu from "../../components/MoreMenu";
import { usersRef, projectsRef } from "../../config/firebase";
@@ -19,32 +20,53 @@ const Research = () => {
const [selected, setSelected] = useState(null);
const [search, setSearch] = useState("");
const queryText = useMemo(() => search.trim(), [search]);
const queryLower = useMemo(
() => (search || "").toLowerCase().trim(),
[search],
);
const [currentUID] = useGlobal("currentUID");
const [autoLoads, setAutoLoads] = useState(0);
const usersQueryRef = useMemo(() => {
try {
if (queryText) {
const lower = queryLower;
if (lower) {
return usersRef
.orderBy("userName")
.startAt(queryText)
.endAt(queryText + "\uf8ff");
.orderBy("userNameLower")
.startAt(lower)
.endAt(lower + "\uf8ff");
}
return usersRef.orderBy("userName");
return usersRef.orderBy("userNameLower");
} catch (e) {
return null;
}
}, [queryLower]);
const projectsQueryRefLower = useMemo(() => {
try {
const lower = queryText ? queryText.toLowerCase() : "";
if (lower) {
return projectsRef
.orderBy("titleLower")
.startAt(lower)
.endAt(lower + "\uf8ff");
}
return null;
} catch (e) {
return null;
}
}, [queryText]);
const projectsQueryRef = useMemo(() => {
const projectsQueryRefCase = useMemo(() => {
try {
if (queryText) {
const raw = queryText || "";
if (raw) {
return projectsRef
.orderBy("title")
.startAt(queryText)
.endAt(queryText + "\uf8ff");
.startAt(raw)
.endAt(raw + "\uf8ff");
}
return projectsRef.orderBy("updatedAt", "desc");
return null;
} catch (e) {
return null;
}
@@ -59,12 +81,19 @@ const Research = () => {
simpleRef: false,
listener: false,
condition: !!usersQueryRef,
refreshArray: [queryText],
refreshArray: [queryLower],
usePagination: true,
batchSize: 5,
});
const filteredUsers = useMemo(() => {
const arr = Array.isArray(userResults) ? userResults : [];
let arr = Array.isArray(userResults) ? userResults : [];
// Client-side safety filter to guarantee match on pseudo
if (queryLower) {
arr = arr.filter((u) => {
const nameLower = (u?.userNameLower || u?.userName || "").toLowerCase();
return nameLower.includes(queryLower);
});
}
const seen = new Set();
const unique = [];
for (const u of arr) {
@@ -75,24 +104,83 @@ const Research = () => {
unique.push(u);
}
return unique;
}, [JSON.stringify(userResults), currentUID]);
}, [JSON.stringify(userResults), queryLower, currentUID]);
const {
data: projectResults = [],
loading: projectsLoading,
loadMore: loadMoreProjects,
data: projectResultsLower = [],
loading: projectsLoadingLower,
loadMore: loadMoreProjectsLower,
} = useDataFromRef({
ref: projectsQueryRef,
ref: projectsQueryRefLower,
simpleRef: false,
listener: false,
condition: !!projectsQueryRef,
condition: !!projectsQueryRefLower,
refreshArray: [queryText],
usePagination: true,
batchSize: 6,
});
const {
data: projectResultsCase = [],
loading: projectsLoadingCase,
loadMore: loadMoreProjectsCase,
} = useDataFromRef({
ref: projectsQueryRefCase,
simpleRef: false,
listener: false,
condition: !!projectsQueryRefCase,
refreshArray: [queryText],
usePagination: true,
batchSize: 6,
});
const {
data: projectResultsDefault = [],
loading: projectsLoadingDefault,
loadMore: loadMoreProjectsDefault,
} = useDataFromRef({
ref: !queryText ? projectsRef.orderBy("updatedAt", "desc") : null,
simpleRef: false,
listener: false,
condition: !queryText,
refreshArray: [queryText],
usePagination: true,
batchSize: 6,
});
const projectsLoading = queryText
? projectsLoadingLower || projectsLoadingCase
: projectsLoadingDefault;
const loadMoreProjects = () => {
if (queryText) {
loadMoreProjectsLower?.();
loadMoreProjectsCase?.();
} else {
loadMoreProjectsDefault?.();
}
};
const filteredProjects = useMemo(() => {
return Array.isArray(projectResults) ? projectResults : [];
}, [JSON.stringify(projectResults)]);
const arr = queryText
? [
...(Array.isArray(projectResultsLower) ? projectResultsLower : []),
...(Array.isArray(projectResultsCase) ? projectResultsCase : []),
]
: Array.isArray(projectResultsDefault)
? projectResultsDefault
: [];
const seen = new Set();
const unique = [];
for (const p of arr) {
const id = p?.id;
if (!id || seen.has(id)) continue;
seen.add(id);
unique.push(p);
}
return unique;
}, [
queryText,
JSON.stringify(projectResultsLower),
JSON.stringify(projectResultsCase),
JSON.stringify(projectResultsDefault),
]);
useEffect(() => {
setAutoLoads(0);
@@ -105,13 +193,7 @@ const Research = () => {
loadMoreUsers();
}
}
}, [
filteredUsers.length,
usersLoading,
usersQueryRef,
autoLoads,
loadMoreUsers,
]);
}, [filteredUsers.length, usersLoading, usersQueryRef, autoLoads]);
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
@@ -124,6 +206,21 @@ const Research = () => {
}
};
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
return (
<Page
headerType="NAVIGATION"
@@ -142,7 +239,15 @@ const Research = () => {
/>
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingTop: 2 }}>
{(!selected || selected === "Musiques") && (
{(!selected &&
!projectsLoading &&
!usersLoading &&
filteredProjects.length === 0 &&
filteredUsers.length === 0) && (
<EmptyText text={"Aucun résultat"} />
)}
{((!selected && (filteredProjects.length > 0 || projectsLoading)) ||
selected === "Musiques") && (
<View style={{ paddingHorizontal: 2 }}>
<CreateLyricsHeader
gradientProps={{
@@ -150,138 +255,144 @@ const Research = () => {
end: { x: 0, y: 1 },
}}
>
<View style={{ gap: 10 }}>
{Array.isArray(filteredProjects) &&
filteredProjects.slice(0, 6).map((p) => (
<MusicCard
key={p.id}
title={p?.title || "Sans titre"}
subtitle={"MusicLand"}
imageUri={p?.coverUrl || null}
projectId={p?.id}
likedBy={p?.likedBy || []}
onPress={() =>
navigate(Routes.MusicDetails, { projectId: p.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
/>
))}
{(filteredProjects.length >= 6 || projectsLoading) && (
<Pressable
onPress={loadMoreProjects}
style={{ alignSelf: "center", marginTop: 6 }}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: projectsLoading ? 0.6 : 1,
}}
>
{projectsLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
)}
</View>
</CreateLyricsHeader>
</View>
)}
{(!selected || selected === "Clips") && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
<View style={{ gap: 6, ...Style.containerRow }}>
{Array.from({ length: 3 }).map((_, index) => (
<View style={{ flex: 1, gap: 6 }} key={index}>
<Image
source={img.placeholder3}
style={{
width: "100%",
height: 147,
borderRadius: 10,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Pour Lili
</Text>
</View>
))}
</View>
</CreateLyricsHeader>
</View>
)}
{(!selected || selected === "Profils") && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<View style={{ gap: 8 }}>
{Array.isArray(filteredUsers) &&
filteredUsers.slice(0, 5).map((u) => (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
key={u.id}
onPress={() =>
navigate(Routes.SingerProfile, { userId: u.id })
}
>
<Image
source={
u?.pictureUrl || u?.profilePictureURL || u?.photoURL
? {
uri:
u?.pictureUrl ||
u?.profilePictureURL ||
u?.photoURL,
}
: img.profile
{filteredProjects.length > 0 ? (
<View style={{ gap: 10 }}>
{Array.isArray(filteredProjects) &&
filteredProjects.slice(0, 6).map((p) => (
<MusicCard
key={p.id}
title={p?.title || "Sans titre"}
subtitle={"MusicLand"}
imageUri={p?.coverUrl || null}
projectId={p?.id}
likedBy={p?.likedBy || []}
onPress={() =>
navigate(Routes.MusicDetails, { projectId: p.id })
}
style={{ ...size({ size: 60 }), borderRadius: 100 }}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuTop(posTop);
setShowMenu(
(prev) => !prev || posTop !== menuTop,
);
}}
/>
))}
{(filteredProjects.length >= 6 || projectsLoading) && (
<Pressable
onPress={loadMoreProjects}
style={{ alignSelf: "center", marginTop: 6 }}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: projectsLoading ? 0.6 : 1,
}}
>
{u?.userName || "Utilisateur"}
{projectsLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
))}
{(filteredUsers.length >= 5 || usersLoading) && (
<Pressable
onPress={loadMoreUsers}
style={{ alignSelf: "center", marginTop: 6 }}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: usersLoading ? 0.6 : 1,
}}
)}
</View>
) : (
!projectsLoading && (
<EmptyText text={"Aucune musique trouvée"} />
)
)}
</CreateLyricsHeader>
</View>
)}
{((!selected && (filteredUsers.length > 0 || usersLoading)) ||
selected === "Profils") && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
{filteredUsers.length > 0 ? (
<View style={{ gap: 8 }}>
{Array.isArray(filteredUsers) &&
filteredUsers.slice(0, 5).map((u) => (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
key={u.id}
onPress={() =>
navigate(Routes.SingerProfile, { userId: u.id })
}
>
<ExpoImage
source={
u?.pictureUrl ||
u?.profilePictureURL ||
u?.photoURL
? {
uri:
u?.pictureUrl ||
u?.profilePictureURL ||
u?.photoURL,
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 60 }),
borderRadius: 100,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{u?.userName || "Utilisateur"}
</Text>
</Pressable>
))}
{(filteredUsers.length >= 5 || usersLoading) && (
<Pressable
onPress={loadMoreUsers}
style={{ alignSelf: "center", marginTop: 6 }}
>
{usersLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
)}
</View>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: usersLoading ? 0.6 : 1,
}}
>
{usersLoading ? "Chargement…" : "Charger plus"}
</Text>
</Pressable>
)}
</View>
) : (
!usersLoading && <EmptyText text={"Aucun profil trouvé"} />
)}
</CreateLyricsHeader>
</View>
)}
{selected === "Playbacks" && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<EmptyText text={"Aucun playback trouvé"} />
</CreateLyricsHeader>
</View>
)}
{selected === "Clips" && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<EmptyText text={"Aucun clip trouvé"} />
</CreateLyricsHeader>
</View>
)}
+23 -10
View File
@@ -1,5 +1,6 @@
import React from "react";
import { FlatList, Image, Pressable, useWindowDimensions } from "react-native";
import { FlatList, Pressable, useWindowDimensions, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { img } from "../../../assets";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
@@ -36,15 +37,27 @@ const LikedMusic = () => {
navigate(Routes.MusicDetails, { projectId: item.id })
}
>
<Image
source={
item?.coverUrl ? { uri: item.coverUrl } : img.placeholder3
}
style={{
height: itemWidth - containerGap,
borderRadius: 10,
}}
/>
{item?.coverUrl ? (
<ExpoImage
source={{ uri: item.coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
height: itemWidth - containerGap,
borderRadius: 10,
}}
/>
) : (
<RNImage
source={img.placeholder3}
style={{
height: itemWidth - containerGap,
borderRadius: 10,
}}
/>
)}
</Pressable>
)}
/>
+35 -15
View File
@@ -1,13 +1,7 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useState } from "react";
import {
Image,
Platform,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useRef, useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { useGlobal } from "reactn";
import { icons, img } from "../../../assets";
import { arrayRemove, arrayUnion, projectsRef } from "../../../config/firebase";
@@ -28,6 +22,7 @@ const MusicCard = ({
const [selected, setSelected] = useState(false);
const [layout, setLayout] = useState(null);
const [menuPos, setMenuPos] = useState(0);
const cardRef = useRef(null);
useEffect(() => {
if (layout) {
@@ -57,6 +52,19 @@ const MusicCard = ({
};
const onPressMenu = () => {
// Prefer absolute screen position when available (Portal rendering)
if (cardRef?.current?.measureInWindow) {
try {
cardRef.current.measureInWindow((x, y, width, height) => {
const top = (y || 0) + 45;
setMenuPos(top);
onPressMore?.(top);
});
return;
} catch (e) {
// fallback to relative layout position
}
}
onPressMore?.(menuPos);
};
@@ -67,13 +75,25 @@ const MusicCard = ({
...Style.containerRow,
gap: 6,
}}
ref={cardRef}
onPress={onPress}
onLayout={(e) => setLayout(e.nativeEvent.layout)}
>
<Image
source={imageUri ? { uri: imageUri } : img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
{imageUri ? (
<ExpoImage
source={{ uri: imageUri }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
)}
<View style={styles.blurContainer}>
<BlurView
intensity={20}
@@ -97,14 +117,14 @@ const MusicCard = ({
}}
>
<Pressable onPress={toggleLike}>
<Image
<RNImage
source={selected ? icons.heart : icons.heartOutline}
style={size({ size: 24 })}
resizeMode="contain"
/>
</Pressable>
<Pressable onPress={onPressMenu}>
<Image source={icons.more} style={size({ size: 24 })} />
<RNImage source={icons.more} style={size({ size: 24 })} />
</Pressable>
</View>
</BlurView>
@@ -6,13 +6,19 @@ import BorderGradient from "../../../components/BorderGradient/BorderGradient";
import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../../../styles/Fonts";
const ResearchHeader = ({ onPress, selected, searchValue = "", onChangeSearch = () => {} }) => {
const ResearchHeader = ({
onPress,
selected,
searchValue = "",
onChangeSearch = () => {},
}) => {
return (
<View style={{ gap: 12 }}>
<SearchBar
textInputProps={{
value: searchValue,
onChangeText: onChangeSearch,
autoFocus: true,
}}
/>
<View
+194
View File
@@ -0,0 +1,194 @@
import React, { useMemo } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import Page from "../layouts/Page";
import { ai, background } from "../assets";
import { BlurView } from "expo-blur";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import palette from "../styles/Palette";
const CREATE_DATA = [
{
img: ai.nathalie,
bg: background.writingBG,
label: "Nathalie",
desc: "Lets write lyrics together !",
type: "Songwriter",
},
{
img: ai.theo,
bg: background.studioBG,
label: "Theo",
desc: "Come back, when\nyoull have lyrics!",
type: "Beatmaker",
},
{
img: ai.bena,
bg: background.productionBG,
label: "Bena",
desc: "Come back when you want\nto publish the song!",
type: "Producer",
},
{
img: ai.john,
bg: background.playbackBG,
label: "John",
desc: "Come back when your\naudio is ready!",
type: "Director",
},
];
const NewMusicOptions = ({ route }) => {
const { userProjects } = useUserData();
const { projectId = null } = route?.params || {};
const currentProjet = useMemo(() => {
if (!projectId) {
return null;
}
return userProjects?.find((p) => p.id === projectId) || null;
}, [projectId, userProjects]);
const hasProject = !!currentProjet;
const hasLyrics = Array.isArray(currentProjet?.lyrics)
? currentProjet.lyrics.length > 0
: !!currentProjet?.lyrics;
const hasCover = !!currentProjet?.coverUrl;
const isLocked = (index) => {
// 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director
if (index === 3) return true; // Director toujours verrouillé
if (!hasProject) return index !== 0; // seulement Songwriter
if (hasCover) return index !== 2; // seulement Producer
if (hasLyrics) return !(index === 0 || index === 1); // Songwriter + Beatmaker
return index !== 0; // par défaut seulement Songwriter
};
const onPressOption = (index, item) => {
if (isLocked(index)) return;
switch (index) {
case 0:
navigate(Routes.WritingLyrics, {});
break;
case 1:
navigate(Routes.Studio, { action: item.type });
break;
case 2:
navigate(Routes.Production, { action: item.type });
break;
default:
break;
}
};
return (
<Page
backgroundImg={background.homeBG}
headerType="NAVIGATION"
title={!!currentProjet ? "Continuer la création" : "Nouvelle musique"}
scrollEnabled={true}
>
<View style={{ gap: 10 }}>
{CREATE_DATA.map((item, index) => {
const locked = isLocked(index);
return (
<Pressable key={index} onPress={() => onPressOption(index, item)}>
<BlurView
tint="dark"
intensity={20}
style={{
position: "absolute",
zIndex: 1,
left: 110,
top: 10,
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 14,
borderBottomLeftRadius: 0,
overflow: "hidden",
backgroundColor: Palette.glass,
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<Text
style={{
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{item.label}, {item.type}
</Text>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item.desc}
</Text>
</BlurView>
<Image
source={item.bg}
style={{
width: "100%",
height: 152,
borderRadius: 20,
position: "absolute",
bottom: 0,
}}
/>
<Image
source={item.img}
style={{ width: 130, height: 200 }}
resizeMode="contain"
/>
{locked && (
<View
pointerEvents="none"
style={{
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.35)",
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.4)",
alignItems: "center",
justifyContent: "center",
}}
>
<FontAwesome
name="lock"
size={24}
color={palette.primary}
/>
</View>
</View>
)}
</Pressable>
);
})}
</View>
</Page>
);
};
export default NewMusicOptions;
+7 -2
View File
@@ -1,7 +1,8 @@
import { BlurView } from "expo-blur";
import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useState } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { Platform, Pressable, Text, View, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, img } from "../../assets";
@@ -145,7 +146,7 @@ const EditProfile = () => {
}
>
<Pressable style={{ alignItems: "center" }} onPress={onChangePicture}>
<Image
<ExpoImage
source={
currentUserData?.profilePictureURL
? {
@@ -153,6 +154,10 @@ const EditProfile = () => {
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 108 }),
borderRadius: 100,
+174
View File
@@ -0,0 +1,174 @@
import React, { useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import { useUser } from "../../providers/UserDataProvider";
import { usersRef } from "../../config/firebase";
import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId";
import useDataFromRef from "../../hooks/useDataFromRef";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { navigate } from "../../navigation/NavigationService";
const Follows = () => {
const { params } = useRoute();
const initial = params?.selected || "Abonnés"; // "Abonnés" | "Abonnements"
const targetUserId = params?.userId || null;
const [selected, setSelected] = useState(initial);
const { currentUID, currentUserData } = useUser();
// Abonnés (followers): docs for IDs in currentUserData.followedBy
// If viewing another user's profile, subscribe to that user's doc
const { data: targetUserDoc = null } = useDataFromRef({
ref: targetUserId ? usersRef.doc(targetUserId) : null,
simpleRef: true,
listener: true,
condition: !!targetUserId,
refreshArray: [targetUserId],
});
const followerIds = useMemo(() => {
if (targetUserId) {
return Array.isArray(targetUserDoc?.followedBy)
? targetUserDoc.followedBy
: [];
}
return Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy
: [];
}, [
targetUserId,
targetUserDoc?.followedBy?.length || 0,
currentUserData?.followedBy?.length || 0,
]);
const { data: followers = [], loading: followersLoading } =
useDataFromArrayId({
ref: usersRef,
arrayId: followerIds,
condition: followerIds.length > 0,
refreshArray: [followerIds.length],
});
// Abonnements (following): users whose followedBy includes currentUID
const { data: following = [], loading: followingLoading } = useDataFromRef({
ref:
targetUserId || currentUID
? usersRef.where(
"followedBy",
"array-contains",
targetUserId || currentUID,
)
: null,
simpleRef: false,
listener: true,
condition: !!(targetUserId || currentUID),
refreshArray: [targetUserId || currentUID],
});
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
const UserRow = ({ user }) => (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
onPress={() => navigate(Routes.SingerProfile, { userId: user?.id })}
>
<ExpoImage
source={
user?.profilePictureURL
? { uri: user.profilePictureURL }
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ ...size({ size: 60 }), borderRadius: 100 }}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{user?.userName || "Utilisateur"}
</Text>
</Pressable>
);
const list = selected === "Abonnés" ? followers : following;
const loading = selected === "Abonnés" ? followersLoading : followingLoading;
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.profileBG}
title={"Abonnés et abonnements"}
contentContainerStyle={{ paddingBottom: 20 }}
>
<View style={{ gap: 12 }}>
<View style={{ ...Style.containerRow, gap: 6 }}>
{["Abonnés", "Abonnements"].map((item) => (
<Pressable
key={item}
onPress={() => setSelected(item)}
style={{ flex: 1 }}
>
<View
style={{
height: 30,
borderRadius: 100,
overflow: "hidden",
backgroundColor:
selected === item ? "#FFFFFF22" : Palette.glass,
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item}
</Text>
</View>
</Pressable>
))}
</View>
<View style={{ gap: 8 }}>
{Array.isArray(list) && list.length > 0 ? (
list.map((u) => <UserRow key={u.id} user={u} />)
) : !loading ? (
<EmptyText
text={
selected === "Abonnés" ? "Aucun abonné" : "Aucun abonnement"
}
/>
) : null}
</View>
</View>
</Page>
);
};
export default Follows;
+80 -78
View File
@@ -1,5 +1,4 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
FlatList,
Image,
@@ -10,6 +9,7 @@ import {
Text,
View,
} from "react-native";
import { Image as ExpoImage } from "expo-image";
import { SheetManager } from "react-native-actions-sheet";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
@@ -20,31 +20,40 @@ import MoreMenu from "../../components/MoreMenu";
import { projectsRef } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { navigate, push } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { getFollowersCount, getFollowingCount } from "../../services/follows";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { gutters, size } from "../../styles/Style";
import MusicCard from "../Library/components/MusicCard";
import { BlurView } from "expo-blur";
const Profile = () => {
const [selected, setSelected] = useState("Chansons");
const [currentUserData] = useGlobal("currentUserData");
const { userProjects, currentUID } = useUser();
const { userProjects, followingCount, currentUserData } = useUser();
const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [following, setFollowing] = useState(0);
const [followers, setFollowers] = useState(0);
const onPressMenu = (item) => {
setSelected(item);
};
useEffect(() => {
if (!currentUID) return;
getFollowersCount(currentUID).then(setFollowers);
getFollowingCount(currentUID).then(setFollowing);
}, [currentUID]);
const EmptyText = ({ text }) => (
<View style={{ flex: 1, ...Style.containerCenter, paddingTop: 20 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
return (
<Page
backgroundImg={background.profileBG}
@@ -76,7 +85,7 @@ const Profile = () => {
}
>
<View style={{ alignItems: "center" }}>
<Image
<ExpoImage
source={
currentUserData?.profilePictureURL
? {
@@ -84,6 +93,10 @@ const Profile = () => {
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 108 }),
borderRadius: 100,
@@ -101,19 +114,27 @@ const Profile = () => {
</View>
<View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>{userProjects?.length || 0}</Text>
<Text style={styles.label}>projets</Text>
</View>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() => push(Routes.Follows, { selected: "Abonnés" })}
>
<Text style={styles.value}>
{currentUserData?.playlist?.length || 0}
{currentUserData?.followedBy?.length || 0}
</Text>
<Text style={styles.label}>playbacks</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>{followers}</Text>
<Text style={styles.label}>abonnés</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>{following}</Text>
</Pressable>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() =>
push(Routes.Follows, { selected: "Abonnements" })
}
>
<Text style={styles.value}>{followingCount}</Text>
<Text style={styles.label}>abonnements</Text>
</View>
</Pressable>
</View>
</BlurView>
</View>
@@ -157,64 +178,39 @@ const Profile = () => {
</View>
{selected === "Playbacks" && (
<View style={{ flex: 1 }}>
<FlatList
data={Array.from({ length: 6 })}
numColumns={3}
contentContainerStyle={{ gap: 14 }}
columnWrapperStyle={{ gap: 10 }}
renderItem={() => (
<Pressable
style={{ flex: 1, gap: 6 }}
onPress={() => navigate(Routes.Reels)}
>
<Image
source={img.placeholder3}
style={{
width: "100%",
height: 147,
borderRadius: 10,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Pour Lili
</Text>
</Pressable>
)}
/>
<EmptyText text={"Aucun playback pour le moment"} />
</View>
)}
{selected === "Chansons" && (
<ScrollView style={{ flex: 1 }}>
<FlatList
data={Array.isArray(userProjects) ? userProjects : []}
contentContainerStyle={{
gap: 10,
}}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={currentUserData?.userName || "MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() =>
navigate(Routes.MusicDetails, { projectId: item.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
/>
)}
/>
{Array.isArray(userProjects) && userProjects.length > 0 ? (
<FlatList
data={userProjects}
contentContainerStyle={{
gap: 10,
}}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={currentUserData?.userName || "MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() =>
navigate(Routes.MusicDetails, { projectId: item.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
/>
)}
/>
) : (
<EmptyText text={"Aucune chanson pour le moment"} />
)}
<MoreMenu
visible={showMenu}
top={menuTop}
@@ -249,13 +245,19 @@ const Profile = () => {
},
},
],
{ cancelable: true }
{ cancelable: true },
),
},
]}
/>
</ScrollView>
)}
{selected === "Clips" && (
<View style={{ flex: 1 }}>
<EmptyText text={"Aucun clip pour le moment"} />
</View>
)}
</View>
</Page>
);
+99 -77
View File
@@ -1,4 +1,3 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useState } from "react";
import {
FlatList,
@@ -9,23 +8,26 @@ import {
Text,
View,
} from "react-native";
import { Image as ExpoImage } from "expo-image";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, icons, img } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
import { followsRef, projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import { projectsRef } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { navigate, push } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { getFollowersCount, getFollowingCount } from "../../services/follows";
import useDataFromRef from "../../hooks/useDataFromRef";
import { usersRef } from "../../config/firebase";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import MusicCard from "../Library/components/MusicCard";
import { BlurView } from "expo-blur";
const SingerProfile = ({ route }) => {
const { userId } = route.params;
const { getUserByUid, followUser, unfollowUser } = useUser();
@@ -37,25 +39,32 @@ const SingerProfile = ({ route }) => {
const fetchUserData = async () => {
const data = userId ? await getUserByUid(userId) : {};
setUserData(data);
const list = Array.isArray(data?.followedBy) ? data.followedBy : [];
setFollowers(list.length);
setIsFollowing(currentUID ? list.includes(currentUID) : false);
};
useEffect(() => {
fetchUserData();
}, [userId]);
useEffect(() => {
if (!userId) return;
getFollowersCount(userId).then(setFollowers);
getFollowingCount(userId).then(setFollowing);
(async () => {
try {
if (currentUID && userId) {
const doc = await followsRef.doc(`${currentUID}_${userId}`).get();
setIsFollowing(!!doc?.exists);
} else {
setIsFollowing(false);
}
} catch (_) {}
})();
}, [userId, currentUID]);
// Live following count for the viewed user
const followingQueryRef = React.useMemo(() => {
try {
return userId
? usersRef.where("followedBy", "array-contains", userId)
: null;
} catch (e) {
return null;
}
}, [userId]);
useDataFromRef({
ref: followingQueryRef,
simpleRef: false,
listener: true,
condition: !!followingQueryRef,
refreshArray: [userId],
onUpdate: (list) => setFollowing(Array.isArray(list) ? list.length : 0),
});
const onPressMenu = (item) => {
if (selected === item) {
setSelected(null);
@@ -85,6 +94,21 @@ const SingerProfile = ({ route }) => {
condition: !!userProjectsQueryRef,
refreshArray: [userId],
});
const EmptyText = ({ text }) => (
<View style={{ flex: 1, ...Style.containerCenter, paddingTop: 20 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
const handleFollowUser = async () => {
if (!currentUID || !userId || currentUID === userId) return;
if (isFollowing) {
@@ -127,7 +151,7 @@ const SingerProfile = ({ route }) => {
}
>
<View style={{ alignItems: "center" }}>
<Image
<ExpoImage
source={
userData?.profilePictureURL
? {
@@ -135,6 +159,10 @@ const SingerProfile = ({ route }) => {
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 108 }),
borderRadius: 100,
@@ -155,16 +183,26 @@ const SingerProfile = ({ route }) => {
<Text style={styles.value}>
{Array.isArray(userProjects) ? userProjects.length : 0}
</Text>
<Text style={styles.label}>playbacks</Text>
<Text style={styles.label}>projets</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() =>
push(Routes.Follows, { selected: "Abonnés", userId })
}
>
<Text style={styles.value}>{followers}</Text>
<Text style={styles.label}>abonnés</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
</Pressable>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() =>
push(Routes.Follows, { selected: "Abonnements", userId })
}
>
<Text style={styles.value}>{following}</Text>
<Text style={styles.label}>abonnements</Text>
</View>
</Pressable>
</View>
{currentUID !== userId && (
<GradientButton
@@ -219,61 +257,45 @@ const SingerProfile = ({ route }) => {
</View>
{selected === "Playbacks" && (
<View style={{ flex: 1 }}>
<FlatList
data={Array.from({ length: 6 })}
numColumns={3}
contentContainerStyle={{ gap: 14 }}
columnWrapperStyle={{ gap: 10 }}
renderItem={() => (
<View style={{ flex: 1, gap: 6 }}>
<Image
source={img.placeholder3}
style={{
width: "100%",
height: 147,
borderRadius: 10,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Pour Lili
</Text>
</View>
)}
/>
<EmptyText text={"Aucun playback pour le moment"} />
</View>
)}
{selected === "Chansons" && (
<View style={{ flex: 1 }}>
<FlatList
data={Array.isArray(userProjects) ? userProjects : []}
contentContainerStyle={{
gap: 10,
}}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={userData?.userName || "MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() =>
navigate(Routes.MusicDetails, { projectId: item.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
/>
)}
/>
{Array.isArray(userProjects) && userProjects.length > 0 ? (
<FlatList
data={userProjects}
contentContainerStyle={{
gap: 10,
}}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={userData?.userName || "MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() =>
navigate(Routes.MusicDetails, { projectId: item.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
/>
)}
/>
) : (
<EmptyText text={"Aucune chanson pour le moment"} />
)}
</View>
)}
{selected === "Clips" && (
<View style={{ flex: 1 }}>
<EmptyText text={"Aucun clip pour le moment"} />
</View>
)}
<MoreMenu
+6 -1
View File
@@ -1,4 +1,5 @@
import { View, Text, Image, ActivityIndicator } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page";
import { background, icons, img } from "../../assets";
@@ -83,8 +84,12 @@ const PouchReady = () => {
<View style={{ flex: 1, ...Style.containerCenter }}>
<View style={{ width: "80%", position: "relative" }}>
{coverUrl ? (
<Image
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={120}
style={{ width: "100%", height: 300, borderRadius: 20 }}
/>
) : (
-2
View File
@@ -6,8 +6,6 @@ import GradientButton from "../../components/GradientButton";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import { gutters } from "../../styles";
import firebase from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { useUser } from "../../providers/UserDataProvider";
import { responsiveHeight } from "react-native-responsive-dimensions";
+61 -44
View File
@@ -1,13 +1,13 @@
import { View, Dimensions } from "react-native";
import React, { useMemo, useRef, useState } from "react";
import { useRoute } from "@react-navigation/native";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton";
import { gutters } from "../../styles";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import Goals from "./Goals";
import { goBack } from "../../navigation/NavigationService";
import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import SpecificityContext from "./SpecificityContext";
import EmotionConvey from "./EmotionConvey";
import SongStyle from "./SongStyle";
@@ -20,42 +20,24 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
const { width } = Dimensions.get("window");
const CreateLyricsWithAi = () => {
const CreateLyricsWithAi = ({ route }) => {
const { hasLyrics = false, regenerateKey } = route?.params || {};
const scrollRef = useRef(null);
const route = useRoute();
const regenerateKey = route?.params?.regenerateKey;
const [selectedIndex, setSelectedIndex] = useState(0);
// If user already has lyrics, start at SongStructure (index 5)
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
// Collected state across steps
const [objective, setObjective] = useState(
__DEV__ ? "Pour mon entreprise" : null,
); // from Goals list
const [objective, setObjective] = useState(null); // from Goals list
const [otherObjective, setOtherObjective] = useState("");
const [context, setContext] = useState(
__DEV__
? "L'agence minuit est une agence de développement mobile et web qui accompagnes ses client dans la réalisations de projets divers et variés"
: "",
);
const [emotion, setEmotion] = useState(
__DEV__
? {
title: "La Joie",
description:
"expose un bonheur profond, l'émerveillement, la gratitude, satisfaction intense, énergie positive",
}
: null,
); // { title, description }
const [style, setStyle] = useState(__DEV__ ? "Upbeat" : null); // from list
const [context, setContext] = useState("");
const [emotion, setEmotion] = useState(null);
const [style, setStyle] = useState(null); // from list
const [otherStyle, setOtherStyle] = useState("");
const [audience, setAudience] = useState(
__DEV__ ? "Aux clients de l'agence minuit" : "",
);
const [structure, setStructure] = useState(
__DEV__ ? "1 couplet, 1 refrain, 1 couplet, 1 refrain" : null,
); // selected structure string
const [rhymes, setRhymes] = useState(__DEV__ ? "Avec rimes" : null);
const [audience, setAudience] = useState("");
const [structure, setStructure] = useState(null); // selected structure string
const [rhymes, setRhymes] = useState(null);
const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain']
const parsedStructure = useMemo(() => {
@@ -117,27 +99,61 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = () => {
setSelectedIndex(selectedIndex + 1);
setProgress(progress + 9);
scrollRef.current.scrollToIndex({
index: selectedIndex + 1,
animated: true,
});
// If user already has lyrics and just finished CustomizeSongStructure (index 6),
// skip AI generation and go straight to Lyrics editor
if (hasLyrics && selectedIndex === 6) {
const chosenStructure =
(customStructure &&
parsedStructure &&
customStructure.length === parsedStructure.length
? customStructure
: parsedStructure) || [];
navigate(Routes.Lyrics, {
config: { structure: chosenStructure },
selections: {
objective,
otherObjective,
context,
emotion,
style,
otherStyle,
audience,
structure,
parsedStructure,
customStructure,
rhymes,
},
hasLyrics: true,
});
return;
}
setSelectedIndex((idx) => idx + 1);
};
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
setProgress(progress - 9);
scrollRef.current.scrollToIndex({
index: selectedIndex - 1,
animated: true,
});
// If currently on SongStructure, go back to previous screen instead of previous step
if (selectedIndex === 5) {
goBack();
} else if (selectedIndex > 0) {
setSelectedIndex((idx) => Math.max(0, idx - 1));
} else {
goBack();
}
};
// Sync scroll position and progress with selectedIndex
React.useEffect(() => {
if (!parentLayout?.height) return;
try {
const nextProgress = 16 + selectedIndex * 9;
setProgress(nextProgress);
scrollRef.current?.scrollToIndex?.({
index: selectedIndex,
animated: true,
});
} catch (_) {}
}, [selectedIndex, parentLayout?.height]);
return (
<Page headerType="NONE">
<MusicLandHeader
@@ -160,6 +176,7 @@ const CreateLyricsWithAi = () => {
style={{ width, left: -gutters }}
ref={scrollRef}
disableGesture
index={selectedIndex}
>
<View
style={{
+1
View File
@@ -168,6 +168,7 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
lyricsData: result,
config,
selections,
hasLyrics: false,
})
}
/>
+85 -70
View File
@@ -1,84 +1,93 @@
import { View, Text, StyleSheet, ScrollView } from "react-native";
import React, { useMemo, useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { View, StyleSheet, Image, Platform, Text } from "react-native";
import React, { useMemo, useState, useCallback, useEffect } from "react";
import { BlurView } from "expo-blur";
import { Palette } from "../../styles";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { CUSTOM_SONG_STRUCTURE } from "../../data/data";
import SongStructureDragDrop from "../../components/SongStructureDragDrop";
import Sortable from "react-native-sortables";
import { icons } from "../../assets";
// baseStructure: array like ['couplet','refrain',...]
// onChange: callback that receives array like ['couplet','refrain',...]
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const [containerLayout, setContainerLayout] = useState(null);
const sourceItems = useMemo(() => {
// create unique ids for duplicates
// Build stable keyed data like { 'couplet-1': { label, value }, ... }
const data = useMemo(() => {
const counters = { couplet: 0, refrain: 0 };
return (baseStructure || []).map((type) => {
const key = (type || '').toLowerCase();
const out = {};
(baseStructure || []).forEach((type) => {
const key = (type || "").toLowerCase();
counters[key] = (counters[key] || 0) + 1;
const idx = counters[key];
return {
id: `${key}-${idx}`,
label: `${type} ${idx}`,
const id = `${key}-${idx}`;
out[id] = {
id,
label: `${key === "couplet" ? "Couplet" : "Refrain"} ${idx}`,
value: key,
};
});
return out;
}, [baseStructure]);
const items = useMemo(() => Object.values(data), [data]);
// Initialize parent with current order (so it's saved even without drag)
useEffect(() => {
const initialValues = (items || []).map((it) => it?.value).filter(Boolean);
onChange?.(initialValues);
// We only want to run when baseStructure-derived items change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items.length]);
const Row = ({ item: rowData }) => (
<View style={styles.itemContainer}>
<BlurView
intensity={30}
style={styles.rowBlur}
experimentalBlurMethod={Platform.OS !== "ios" ? "dimezisBlurView" : "none"}
>
<View style={{ ...Style.containerSpaceBetween, alignItems: "center" }}>
<View>
<View>
<View>
<View />
</View>
</View>
<View>
<View />
</View>
</View>
<View style={{ flex: 1, justifyContent: "center" }}>
<View>
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
</View>
</View>
<Sortable.Handle>
<Image source={icons.dragDots} style={styles.handleIcon} />
</Sortable.Handle>
</View>
</BlurView>
</View>
);
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title="Personnalise la structure de ta chanson"
subTitle="Intègre en Drag and drop"
subTitle="Réorganise par glisser-déposer"
/>
{/* <View style={{ flex: 1, gap: 10 }}>
<View
style={{ flex: 1 }}
onLayout={(e) => {
setContainerLayout(e.nativeEvent.layout);
<View style={{ flex: 1 }} onLayout={(e) => setContainerLayout(e.nativeEvent.layout)}>
<Sortable.Grid
columns={1}
rowGap={12}
data={items}
renderItem={({ item }) => <Row item={item} />}
customHandle
onDragEnd={({ data: newData }) => {
const values = (newData || []).map((it) => it?.value).filter(Boolean);
onChange?.(values);
}}
>
<ItemContainer height={containerLayout?.height}>
<ScrollView contentContainerStyle={{ gap: 10, paddingBottom: 20 }}>
{["Couplet", "Refrain", "Couplet", "Refrain"].map(
(item, index) => (
<View key={index} style={styles.dropzoneContainer}>
<Text style={styles.dropzone}>{item}</Text>
</View>
)
)}
</ScrollView>
</ItemContainer>
</View>
<View style={{ flex: 1 }}>
<ItemContainer height={containerLayout?.height}>
<ScrollView contentContainerStyle={{ gap: 10, paddingBottom: 20 }}>
{CUSTOM_SONG_STRUCTURE.map((item, index) => (
<View key={index} style={styles.itemContainer}>
<BlurView
intensity={30}
style={{
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
}}
>
<Text style={styles.dropzone}>{item}</Text>
</BlurView>
</View>
))}
</ScrollView>
</ItemContainer>
</View>
</View> */}
<View style={{ flex: 1 }}>
<SongStructureDragDrop
sourceItems={sourceItems}
onChange={(arr) => onChange?.(arr)}
/>
</View>
</View>
@@ -88,17 +97,6 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
export default CustomizeSongStructure;
const styles = StyleSheet.create({
dropzoneContainer: {
paddingVertical: 14,
paddingHorizontal: 12,
backgroundColor: "#00000080",
borderRadius: 14,
},
dropzone: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
@@ -113,4 +111,21 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
rowBlur: {
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
height: "100%",
},
rowText: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
handleIcon: {
width: 22,
height: 22,
tintColor: Palette.white,
},
});
+6 -4
View File
@@ -1,13 +1,15 @@
import { View, Text, FlatList } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { EMOTION_CONVEY } from "../../data/data";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const EmotionConvey = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
const EmotionConvey = ({
selected: selectedProp,
setSelected: setSelectedProp,
}) => {
const [containerLayout, setContainerLayout] = useState(null);
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
@@ -25,8 +27,8 @@ const EmotionConvey = ({ selected: selectedProp, setSelected: setSelectedProp })
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title={`Quel émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne tes intentions émotionnelles. (2 max)"
title={`Quelle émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne une seule intention émotionnelle."
/>
<View
+43 -14
View File
@@ -1,17 +1,16 @@
import { View, Text, ScrollView, Alert } from "react-native";
import { View, ScrollView, Alert } from "react-native";
import React, { useMemo, useState, useCallback } from "react";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { gutters } from "../../styles";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import CustomInput from "./components/CustomInput";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { useRoute } from "@react-navigation/native";
import firebase from "../../config/firebase";
import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
const Lyrics = ({ navigation }) => {
@@ -20,17 +19,30 @@ const Lyrics = ({ navigation }) => {
const lyricsData = route?.params?.lyricsData;
const config = route?.params?.config;
const selections = route?.params?.selections;
const hasLyrics = !!route?.params?.hasLyrics;
const { setIsLoading } = useMinuit();
const initial = useMemo(() => {
const title = lyricsData?.title || "";
const aiSections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
const aiSections = Array.isArray(lyricsData?.lyrics)
? lyricsData.lyrics
: [];
// Respecter l'ordre de la structure choisie si disponible
const targetStructure = Array.isArray(config?.structure)
? config.structure.map((t) => (t || "").toLowerCase())
: null;
if (aiSections.length && targetStructure && aiSections.length === targetStructure.length) {
return { title, sections: aiSections.map((s) => ({ type: (s?.type || "").toLowerCase(), lyrics: s?.lyrics || "" })) };
if (
aiSections.length &&
targetStructure &&
aiSections.length === targetStructure.length
) {
return {
title,
sections: aiSections.map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
})),
};
}
// Sinon, créer à partir de la structure
if (targetStructure && targetStructure.length) {
@@ -77,9 +89,23 @@ const Lyrics = ({ navigation }) => {
const onValidate = useCallback(async () => {
try {
await setIsLoading(true);
const titleTrimmed = (titleValue || "").trim();
if (!titleTrimmed) {
Alert.alert("Titre manquant", "Veuillez renseigner un titre.");
return;
}
const invalid = (sections || []).some((s) => !(s?.lyrics || "").trim());
if (invalid) {
Alert.alert(
"Champs incomplets",
"Chaque couplet et refrain doit contenir du texte.",
);
return;
}
const user = firebase.auth().currentUser;
const payload = {
title: titleValue?.trim() || "",
title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(),
lyrics: (sections || []).map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
@@ -89,9 +115,10 @@ const Lyrics = ({ navigation }) => {
userId: user ? user.uid : null,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
hasLyrics,
};
await firebase.firestore().collection("projects").add(payload);
navigate(Routes.Studio);
const { id: projectId } = await projectsRef.add(payload);
navigate(Routes.FlowSelection, { projectId });
} catch (e) {
console.log(e);
Alert.alert("Erreur", "Échec de l'enregistrement dans le projet.");
@@ -157,10 +184,12 @@ const Lyrics = ({ navigation }) => {
gap: 12,
}}
>
<BorderGradientButton
title="Générer d'autre paroles"
onPress={regenerate}
/>
{!hasLyrics && (
<BorderGradientButton
title="Générer d'autres paroles"
onPress={regenerate}
/>
)}
<GradientButton title="Valider" onPress={onValidate} />
</View>
</Page>
+7 -5
View File
@@ -1,13 +1,15 @@
import { View, Text, StyleSheet, FlatList, Pressable } from "react-native";
import { View, Text, StyleSheet, FlatList } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { BlurView } from "expo-blur";
import { GOALS, SONG_STRUCTURE } from "../../data/data";
import { Palette, Style } from "../../styles";
import { SONG_STRUCTURE } from "../../data/data";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const SongStructure = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
const SongStructure = ({
selected: selectedProp,
setSelected: setSelectedProp,
}) => {
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected;
+6 -1
View File
@@ -8,7 +8,12 @@ import CustomInput from "./components/CustomInput";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const SongStyle = ({ selected: selectedProp, setSelected: setSelectedProp, otherStyle, setOtherStyle }) => {
const SongStyle = ({
selected: selectedProp,
setSelected: setSelectedProp,
otherStyle,
setOtherStyle,
}) => {
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected;
+8 -2
View File
@@ -13,7 +13,7 @@ const WritingLyrics = () => {
return (
<Page headerType="NONE">
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
<MusicLandHeader showSkip onPressBack={goBack} progress={9} />
<MusicLandHeader onPressBack={goBack} progress={9} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
@@ -24,7 +24,13 @@ const WritingLyrics = () => {
gap: 12,
}}
>
<BorderGradientButton />
<BorderGradientButton
onPress={() => {
navigate(Routes.CreateLyricsWithAi, {
hasLyrics: true,
});
}}
/>
<GradientButton
title="Écrire des paroles avec une IA"
onPress={() => navigate(Routes.CreateLyricsWithAi)}