continue generating flow, add backend connection on main tabs

This commit is contained in:
Thomas Demirdjian
2025-08-28 17:18:28 +02:00
parent 0ca7544005
commit bb7b35626f
110 changed files with 1701 additions and 3408 deletions
+256 -41
View File
@@ -6,7 +6,7 @@ import {
Pressable,
StyleSheet,
} from "react-native";
import React, { useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Page from "../../layouts/Page";
import { background, icons, img } from "../../assets";
import { Palette } from "../../styles";
@@ -15,16 +15,169 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import Slider from "../../components/Slider";
import { useRoute } from "@react-navigation/core";
import { SheetManager } from "react-native-actions-sheet";
import firebase, { usersRef, projectsRef, arrayUnion, arrayRemove } from "../../config/firebase";
import { useGlobal } from "reactn";
import useDataFromRef from "../../hooks/useDataFromRef";
import { Audio } from "expo-av";
const MusicDetails = () => {
const params = useRoute().params;
const params = useRoute().params || {};
const action = params?.action;
const projectId = params?.projectId || null;
const [fav, setFav] = useState(false);
const [currentUID] = useGlobal("currentUID");
const [isPlaying, setIsPlaying] = useState(false);
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const wasPlayingBeforeSeek = React.useRef(false);
const { data: project } = useDataFromRef({
ref: projectId
? firebase.firestore().collection("projects").doc(projectId)
: null,
simpleRef: true,
listener: true,
condition: !!projectId,
});
const { data: owner } = useDataFromRef({
ref: project?.userId ? usersRef.doc(project.userId) : null,
simpleRef: true,
listener: true,
condition: !!project?.userId,
});
useEffect(() => {
if (project && currentUID) {
const liked = Array.isArray(project?.likedBy)
? project.likedBy.includes(currentUID)
: false;
setFav(liked);
}
}, [project?.likedBy, currentUID]);
const title = project?.title || "Sans titre";
const artist = owner?.userName || "MusicLand";
const coverUrl = project?.coverUrl || null;
const songUrl = useMemo(() => {
if (project?.song?.url) return project.song.url;
const arr = Array.isArray(project?.musicUrls) ? project.musicUrls : [];
return arr[0] || null;
}, [project]);
const soundRef = React.useRef(null);
// Load/unload audio with expo-av for reliable status updates on iOS/Android
useEffect(() => {
let isMounted = true;
const load = async () => {
try {
// Unload previous
if (soundRef.current) {
await soundRef.current.unloadAsync();
soundRef.current.setOnPlaybackStatusUpdate(null);
soundRef.current = null;
}
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);
},
);
soundRef.current = sound;
} catch (e) {
console.log("Audio load error", e?.message);
}
};
load();
return () => {
isMounted = false;
(async () => {
try {
if (soundRef.current) {
await soundRef.current.unloadAsync();
soundRef.current.setOnPlaybackStatusUpdate(null);
soundRef.current = null;
}
} catch (_) {}
})();
};
}, [songUrl]);
const fmt = (ms) => {
const total = Math.max(0, Math.floor((ms || 0) / 1000));
const m = Math.floor(total / 60)
.toString()
.padStart(1, "0");
const s = (total % 60).toString().padStart(2, "0");
return `${m}:${s}`;
};
const togglePlay = async () => {
const sound = soundRef.current;
if (!sound || !songUrl) return;
try {
const status = await sound.getStatusAsync();
if (status?.isLoaded && status.isPlaying) {
await sound.pauseAsync();
setIsPlaying(false);
} else {
await sound.playAsync();
setIsPlaying(true);
}
} catch (e) {
console.log("MusicDetails audio error", e?.message);
}
};
const onSeek = async (ratio) => {
try {
const dur = progressInfo.dur || 0;
const pos = Math.floor(dur * ratio);
const sound = soundRef.current;
if (sound && dur > 0) {
await sound.setPositionAsync(pos);
}
} catch (e) {
console.log("MusicDetails seek error", e?.message);
}
};
const seekBy = async (deltaSeconds) => {
try {
const cur = Math.floor((progressInfo.pos || 0) / 1000);
const next = Math.max(0, cur + deltaSeconds);
const sound = soundRef.current;
if (sound) await sound.setPositionAsync(next * 1000);
} catch (e) {
console.log("MusicDetails seekBy error", e?.message);
}
};
const description = useMemo(() => {
// Try to build a readable text from lyrics if present
if (Array.isArray(project?.lyrics)) {
return project.lyrics
.map((s) => (s?.lyrics || "").trim())
.filter(Boolean)
.slice(0, 4)
.join("\n\n");
}
const c = project?.lyrics?.couplet;
const r = project?.lyrics?.refrain;
const parts = [c, r].filter(Boolean);
return parts.length ? parts.join("\n\n") : "";
}, [project]);
return (
<Page
headerType="NAVIGATION"
title={action === "userProfile" ? "Mon profil" : "Recherche"}
title={action === "userProfile" ? "Mon profil" : "Détail musique"}
backgroundImg={
action === "userProfile" ? background.profileBG : background.libraryBG2
}
@@ -48,14 +201,32 @@ const MusicDetails = () => {
// stickyHeaderIndices={[1]}
>
<View style={{ gap: 28 }}>
<Image source={img.placeholder4} style={styles.img} />
<Image
source={coverUrl ? { uri: coverUrl } : img.placeholder4}
style={styles.img}
/>
<View style={{ ...Style.containerSpaceBetween }}>
<View>
<Text style={styles.title}>Lust for Life</Text>
<Text style={styles.name}>Lana del Rey</Text>
<Text style={styles.title}>{title}</Text>
<Text style={styles.name}>{artist}</Text>
</View>
<View style={{ ...Style.containerRow, gap: 12 }}>
<Pressable onPress={() => setFav(!fav)}>
<Pressable
onPress={async () => {
if (!projectId || !currentUID) return;
const next = !fav;
setFav(next);
try {
await projectsRef.doc(projectId).update({
likedBy: next
? arrayUnion(currentUID)
: arrayRemove(currentUID),
});
} catch (e) {
setFav(!next);
}
}}
>
<Image
source={fav ? icons.heart : icons.heartOutline}
style={{ ...size({ size: 24 }) }}
@@ -73,47 +244,91 @@ const MusicDetails = () => {
</View>
</View>
<View style={{ paddingTop: 22 }}>
<Slider value={"0:00"} maxValue={"2:00"} />
<Slider
value={fmt(progressInfo.pos)}
maxValue={fmt(progressInfo.dur)}
progress={
progressInfo.dur ? (progressInfo.pos || 0) / progressInfo.dur : 0
}
seekEnabled={!!songUrl}
onSeekStart={async () => {
try {
const sound = soundRef.current;
const status = await sound?.getStatusAsync?.();
wasPlayingBeforeSeek.current =
!!status?.isLoaded && !!status?.isPlaying;
if (status?.isLoaded && status?.isPlaying) {
await sound.pauseAsync();
setIsPlaying(false);
}
} catch (e) {
console.log("Pause on seek start error", e?.message);
}
}}
onSeek={onSeek}
onSeekEnd={async () => {
try {
const sound = soundRef.current;
if (sound && wasPlayingBeforeSeek.current) {
await sound.playAsync();
setIsPlaying(true);
}
wasPlayingBeforeSeek.current = false;
} catch (e) {
console.log("Resume after seek error", e?.message);
}
}}
/>
<View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}>
<Pressable>
<Image source={icons.forward} />
</Pressable>
<Pressable>
<Image source={icons.pause} />
</Pressable>
<Pressable>
{/* Previous (rewind 10s) */}
<Pressable onPress={() => seekBy(-10)}>
<Image
source={icons.forward}
style={{ transform: [{ rotate: "180deg" }] }}
style={{
...size({ size: 30 }),
}}
/>
</Pressable>
{/* Play / Pause */}
<Pressable
style={{
...size({ size: 40 }),
alignItems: "center",
justifyContent: "center",
}}
onPress={togglePlay}
>
<Image
resizeMode={"contain"}
source={isPlaying ? icons.pause : icons.play}
style={size({ size: 34 })}
/>
</Pressable>
{/* Next (forward 10s) */}
<Pressable onPress={() => seekBy(10)}>
<Image
source={icons.forward}
style={{
...size({ size: 30 }),
transform: [{ rotate: "180deg" }],
}}
/>
</Pressable>
</View>
</View>
<View style={{ marginTop: 30, gap: 20 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Description chanson. Viverra enim risus enim enim placerat. Integer
pulvinar tristique suscipit risus. Id hendrerit in odio phasellus
interdum lectus amet
</Text>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Allô{"\n"}Écoute maman est près de toi{"\n"}Il faut lui dire "maman,
c'est quelqu'un pour toi"{"\n"}Ah, c'est le monsieur de la dernière
fois{"\n"}Bon, je vais la chercher{"\n"}Je crois qu'elle est dans
son bain{"\n"}Et je sais pas si elle va pouvoir venir
</Text>
</View>
{description?.length > 0 && (
<View style={{ marginTop: 30, gap: 20 }}>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{description}
</Text>
</View>
)}
</ScrollView>
</Page>
);