playback
This commit is contained in:
+226
-112
@@ -1,126 +1,240 @@
|
||||
import { View, Text, Image, Pressable, Platform } from "react-native";
|
||||
import React from "react";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { VideoView, useVideoPlayer } from "expo-video";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { icons, img } from "../assets";
|
||||
import { size } from "../styles/Style";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { projectsRef } from "../config/firebase";
|
||||
import useDataFromRef from "../hooks/useDataFromRef";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { size } from "../styles/Style";
|
||||
|
||||
const PlaybackItem = ({ item, isActive }) => {
|
||||
const videoUrl = item?.playbackUrl || null;
|
||||
const audioSource = useMemo(() => {
|
||||
const fromSong = item?.songUrl ? { uri: item.songUrl } : null;
|
||||
return fromSong;
|
||||
}, [item]);
|
||||
|
||||
const hasExternalAudio = !!audioSource;
|
||||
console.log("has external audio : ", hasExternalAudio);
|
||||
|
||||
const audioPlayer = useAudioPlayer(audioSource || undefined);
|
||||
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
||||
p.loop = false; // vidéo et musique ont la même durée
|
||||
p.muted = !!hasExternalAudio; // mute seulement si audio externe
|
||||
p.timeUpdateEventInterval = 0.2;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const toggle = async () => {
|
||||
try {
|
||||
if (isActive) {
|
||||
try {
|
||||
if (audioPlayer && hasExternalAudio) await audioPlayer.seekTo?.(0);
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.currentTime = 0;
|
||||
} catch (e) {}
|
||||
|
||||
// Lancer quasi simultanément (éviter await pour limiter le décalage)
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.play();
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (audioPlayer && hasExternalAudio) audioPlayer.play?.();
|
||||
} catch (e) {}
|
||||
} else {
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
toggle();
|
||||
}, [isActive, audioPlayer, videoPlayer, hasExternalAudio]);
|
||||
|
||||
// Micro-correction initiale uniquement pour absorber un léger décalage réseau
|
||||
useEffect(() => {
|
||||
if (!isActive || !hasExternalAudio) return;
|
||||
const t = setTimeout(() => {
|
||||
try {
|
||||
const a = audioPlayer?.currentTime || 0;
|
||||
const v = videoPlayer?.currentTime || 0;
|
||||
if (Math.abs(a - v) > 0.2 && videoPlayer) {
|
||||
videoPlayer.currentTime = Math.max(0, a);
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
if (audioPlayer?.playing) audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
} catch (e) {}
|
||||
};
|
||||
}, [audioPlayer, videoPlayer]);
|
||||
|
||||
const Playbacks = () => {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Carousel
|
||||
data={Array.from({ length: 5 })}
|
||||
vertical
|
||||
height={responsiveHeight(100)}
|
||||
renderItem={() => (
|
||||
<View
|
||||
style={{
|
||||
height: responsiveHeight(100),
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: responsiveHeight(100),
|
||||
position: "relative",
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
>
|
||||
{!!videoUrl ? (
|
||||
<VideoView
|
||||
player={videoPlayer}
|
||||
nativeControls={false}
|
||||
contentFit="cover"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
source={img.placeholder3}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Right side actions */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
bottom: 130,
|
||||
gap: 18,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
paddingHorizontal: 13,
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 6, alignItems: "center" }}>
|
||||
<Pressable>
|
||||
<Image
|
||||
source={img.profile}
|
||||
style={{
|
||||
...size({ size: 45 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.white,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
Suivre
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable>
|
||||
<Image
|
||||
source={img.placeholder3}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
source={icons.heartOutline}
|
||||
style={size({ size: 26 })}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<Image source={icons.share} style={size({ size: 26 })} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={{ paddingHorizontal: 28 }}>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: Palette.glass,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
bottom: 130,
|
||||
gap: 18,
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
paddingHorizontal: 13,
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 6, alignItems: "center" }}>
|
||||
<Pressable>
|
||||
<Image
|
||||
source={img.profile}
|
||||
style={{
|
||||
...size({ size: 45 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.white,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
Suivre
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable>
|
||||
<Image
|
||||
source={icons.heartOutline}
|
||||
style={size({ size: 26 })}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<Image source={icons.share} style={size({ size: 26 })} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={{ paddingHorizontal: 28 }}>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: Palette.glass,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
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
|
||||
</Text>
|
||||
</BlurView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{item?.title || "Description chanson"}
|
||||
</Text>
|
||||
</BlurView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const Playbacks = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { data: playbacks = [], loadMore } = useDataFromRef({
|
||||
ref: projectsRef.where("playbackUrl", "!=", null),
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
usePagination: true,
|
||||
batchSize: 2,
|
||||
});
|
||||
|
||||
const onSnap = useCallback(
|
||||
(index) => {
|
||||
setActiveIndex(index);
|
||||
// Pré-charge la suite 2 par 2
|
||||
if (index >= (playbacks?.length || 0) - 2) {
|
||||
loadMore?.();
|
||||
}
|
||||
},
|
||||
[playbacks?.length, loadMore]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
||||
<Carousel
|
||||
data={playbacks}
|
||||
vertical
|
||||
height={responsiveHeight(100)}
|
||||
pagingEnabled
|
||||
windowSize={5}
|
||||
onSnapToItem={onSnap}
|
||||
renderItem={({ item, index }) => (
|
||||
<PlaybackItem
|
||||
key={item?.id || index}
|
||||
item={item}
|
||||
isActive={index === activeIndex}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user