global player

This commit is contained in:
2025-10-07 09:51:38 +02:00
parent 5cd6741ae6
commit f796d91267
15 changed files with 1031 additions and 136 deletions
+194
View File
@@ -0,0 +1,194 @@
import { BlurView } from "expo-blur";
import React, { useMemo } from "react";
import {
Image,
Platform,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { icons, img } from "../../assets";
import usePlayer from "../../hooks/usePlayer";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
const formatDuration = (ms) => {
const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000));
const minutes = Math.floor(totalSeconds / 60)
.toString()
.padStart(1, "0");
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
return `${minutes}:${seconds}`;
};
const GlobalAudioPlayer = () => {
const { currentTrack, isPlaying, positionMs, durationMs, pause, resume } =
usePlayer();
const insets = useSafeAreaInsets();
const hasTrack = !!currentTrack?.source;
if (!hasTrack) return null;
const title = currentTrack?.title || "Lecture en cours";
const artist = currentTrack?.artist || "";
const progress =
durationMs > 0 ? Math.min(1, Math.max(0, positionMs / durationMs)) : 0;
const artworkSource = useMemo(() => {
if (typeof currentTrack?.artwork === "number") return currentTrack.artwork;
if (currentTrack?.artwork && typeof currentTrack.artwork === "string") {
return { uri: currentTrack.artwork };
}
if (currentTrack?.coverUrl && typeof currentTrack.coverUrl === "string") {
return { uri: currentTrack.coverUrl };
}
return img.placeholder;
}, [currentTrack?.artwork, currentTrack?.coverUrl]);
const handleToggle = async () => {
if (isPlaying) {
await pause();
} else {
await resume();
}
};
return (
<View pointerEvents="box-none" style={styles.host}>
<BlurView
intensity={Platform.OS === "ios" ? 40 : 15}
tint="dark"
style={[
styles.container,
{
paddingBottom: Math.max(insets.bottom, 12),
},
]}
>
<Image source={artworkSource} style={styles.cover} />
<View style={styles.details}>
<Text numberOfLines={1} style={styles.title}>
{title}
</Text>
{!!artist && (
<Text numberOfLines={1} style={styles.artist}>
{artist}
</Text>
)}
<View style={styles.progressContainer}>
<View style={styles.progressTrack}>
<View
style={[styles.progressFill, { width: `${progress * 100}%` }]}
/>
</View>
<View style={styles.timeContainer}>
<Text style={styles.time}>{formatDuration(positionMs)}</Text>
<Text style={styles.time}>{formatDuration(durationMs)}</Text>
</View>
</View>
</View>
<View style={styles.actions}>
<Pressable
onPress={handleToggle}
hitSlop={12}
style={styles.toggleButton}
>
<Image
source={isPlaying ? icons.pause : icons.play}
style={styles.toggleIcon}
/>
</Pressable>
</View>
</BlurView>
</View>
);
};
const styles = StyleSheet.create({
host: {
position: "absolute",
left: 0,
right: 0,
bottom: 0,
pointerEvents: "box-none",
},
container: {
marginHorizontal: 16,
marginBottom: 12,
borderRadius: 20,
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingHorizontal: 16,
paddingTop: 12,
backgroundColor: "rgba(18, 11, 29, 0.85)",
borderWidth: 1,
borderColor: "#372152",
},
cover: {
width: 48,
height: 48,
borderRadius: 12,
backgroundColor: "#1f1829",
},
details: {
flex: 1,
gap: 6,
},
title: {
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 14,
},
artist: {
color: "#C2B7D6",
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
},
progressContainer: {
gap: 6,
},
progressTrack: {
height: 4,
borderRadius: 8,
backgroundColor: "rgba(255,255,255,0.1)",
overflow: "hidden",
},
progressFill: {
height: 4,
backgroundColor: "#F94697",
},
timeContainer: {
flexDirection: "row",
justifyContent: "space-between",
},
time: {
color: "#C2B7D6",
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 10,
},
actions: {
flexDirection: "row",
alignItems: "center",
},
toggleButton: {
width: 44,
height: 44,
borderRadius: 22,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
toggleIcon: {
width: 24,
height: 24,
tintColor: Palette.white,
resizeMode: "contain",
},
});
export default GlobalAudioPlayer;