Change main page and modify flows
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -23,6 +23,7 @@ const Library = () => {
|
||||
<SearchBar
|
||||
textInputProps={{
|
||||
editable: false,
|
||||
onPress: () => navigate(Routes.Research),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user