playback styles

This commit is contained in:
2025-09-30 11:55:54 +02:00
parent fc17b09e7a
commit 81ddbb2cd4
2 changed files with 201 additions and 136 deletions
+19 -4
View File
@@ -1,22 +1,37 @@
import * as Haptics from "expo-haptics"; import * as Haptics from "expo-haptics";
import React from "react"; import React, { useMemo } from "react";
import { Text, TouchableOpacity, View } from "react-native"; import { Text, TouchableOpacity, View } from "react-native";
import { useGlobal } from "reactn";
import ItemContainer from "../components/ItemContainer/ItemContainer"; import ItemContainer from "../components/ItemContainer/ItemContainer";
import useLayoutType from "../hooks/useLayoutType"; import useLayoutType from "../hooks/useLayoutType";
import { gutters, Palette, Style } from "../styles"; import { gutters, Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts"; import { FONT_FAMILY } from "../styles/Fonts";
const TabBar = ({ state = {}, descriptors = {}, navigation = {} }) => { const TabBar = ({ state = {}, descriptors = {}, navigation = {} }) => {
const { isDesktop, windowWidth, mediumDesktopBreakpoint, isWeb } = const { windowWidth, isWeb } = useLayoutType();
useLayoutType(); const [webLayoutMode] = useGlobal("webLayoutMode");
const containerWidth = useMemo(() => {
if (!isWeb) return "90%";
const containerFraction = webLayoutMode === "playbacks-wide" ? 0.9 : 0.6;
const containerMax = webLayoutMode === "playbacks-wide" ? 1440 : 800;
const computedContainerWidth = Math.min(
windowWidth * containerFraction,
containerMax
);
const barWidth = Math.max(computedContainerWidth * 0.9, 360);
return barWidth;
}, [isWeb, webLayoutMode, windowWidth]);
return ( return (
<View <View
style={{ style={{
position: "absolute", position: "absolute",
bottom: gutters * 2, bottom: gutters * 2,
width: "90%", width: containerWidth,
maxWidth: 700,
alignSelf: "center", alignSelf: "center",
zIndex: 1, zIndex: 1,
justifyContent: "center", justifyContent: "center",
+182 -132
View File
@@ -23,7 +23,6 @@ import {
useWindowDimensions, useWindowDimensions,
} from "react-native"; } from "react-native";
import Carousel from "react-native-reanimated-carousel"; import Carousel from "react-native-reanimated-carousel";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { setGlobal } from "reactn"; import { setGlobal } from "reactn";
import { icons, img } from "../../assets"; import { icons, img } from "../../assets";
import KaraokeLyrics from "../../components/KaraokeLyrics"; import KaraokeLyrics from "../../components/KaraokeLyrics";
@@ -67,6 +66,7 @@ const CommentsPanel = ({
commentsCount, commentsCount,
onCommentAdded, onCommentAdded,
inputRef, inputRef,
panelHeight,
}) => { }) => {
const { currentUID, currentUserData } = useUser() || {}; const { currentUID, currentUserData } = useUser() || {};
const [text, setText] = useState(""); const [text, setText] = useState("");
@@ -177,98 +177,113 @@ const CommentsPanel = ({
<BlurView <BlurView
intensity={Platform.OS !== "ios" ? 10 : 20} intensity={Platform.OS !== "ios" ? 10 : 20}
tint="dark" tint="dark"
style={styles.commentsWrapper} style={[
styles.commentsWrapper,
panelHeight ? { height: panelHeight } : null,
]}
> >
<ScrollView <View style={styles.commentsInner}>
ref={scrollRef} <ScrollView
contentContainerStyle={styles.commentsContent} ref={scrollRef}
keyboardDismissMode="interactive" style={styles.commentsScroll}
keyboardShouldPersistTaps="handled" contentContainerStyle={styles.commentsContent}
onScroll={handleScroll} keyboardDismissMode="interactive"
scrollEventThrottle={120} keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false} onScroll={handleScroll}
> scrollEventThrottle={120}
<Text style={styles.sectionTitle}>Description</Text> showsVerticalScrollIndicator={false}
<View style={styles.descriptionCard}>
<Text style={styles.descriptionText}>
{descriptionText || "Description chanson..."}
</Text>
</View>
<Text style={[styles.sectionTitle, styles.sectionSpacing]}>
{`Commentaires${commentsCount ? ` (${commentsCount})` : ""}`}
</Text>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c, index) => (
<View
key={c.id}
style={[styles.commentRow, index > 0 && styles.commentRowSpacing]}
>
{c?.profilePicture ? (
<ExpoImage
source={{ uri: c.profilePicture }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={styles.commentAvatar}
/>
) : (
<View style={styles.commentAvatarFallback} />
)}
<View style={styles.commentBubble}>
<View style={styles.commentHeader}>
<Text style={styles.commentAuthor}>
{c?.userId === currentUID ? "vous" : c?.userName || ""}
</Text>
<Text style={styles.commentMeta}>
{formatRelativeTime(c?.createdAt)}
</Text>
</View>
<Text style={styles.commentBody}>{c?.text}</Text>
</View>
</View>
))
) : (
<Text style={styles.emptyState}>
Aucun commentaire pour le moment
</Text>
)}
</ScrollView>
<View style={styles.commentInputContainer}>
<TextInput
ref={inputRef}
value={text}
onChangeText={setText}
placeholder={
canComment ? "Commente" : "Connecte-toi pour laisser un commentaire"
}
placeholderTextColor="#FFFFFF90"
editable={canComment}
style={[styles.commentInput, !canComment && { opacity: 0.6 }]}
/>
<Pressable
onPress={onSend}
disabled={!canComment || !text.trim()}
style={({ pressed }) => [
styles.sendButton,
(!canComment || !text.trim()) && styles.sendButtonDisabled,
pressed && canComment && styles.sendButtonPressed,
]}
> >
<Image <Text style={styles.sectionTitle}>Description</Text>
source={icons.send} <View style={styles.descriptionCard}>
style={styles.sendIcon} <Text style={styles.descriptionText}>
resizeMode="contain" {descriptionText || "Description chanson..."}
</Text>
</View>
<Text style={[styles.sectionTitle, styles.sectionSpacing]}>
{`Commentaires${commentsCount ? ` (${commentsCount})` : ""}`}
</Text>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c, index) => (
<View
key={c.id}
style={[
styles.commentRow,
index > 0 && styles.commentRowSpacing,
]}
>
{c?.profilePicture ? (
<ExpoImage
source={{ uri: c.profilePicture }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={styles.commentAvatar}
/>
) : (
<View style={styles.commentAvatarFallback} />
)}
<View style={styles.commentBubble}>
<View style={styles.commentHeader}>
<Text style={styles.commentAuthor}>
{c?.userId === currentUID ? "vous" : c?.userName || ""}
</Text>
<Text style={styles.commentMeta}>
{formatRelativeTime(c?.createdAt)}
</Text>
</View>
<Text style={styles.commentBody}>{c?.text}</Text>
</View>
</View>
))
) : (
<Text style={styles.emptyState}>
Aucun commentaire pour le moment
</Text>
)}
</ScrollView>
<View style={styles.commentInputContainer}>
<TextInput
ref={inputRef}
value={text}
onChangeText={setText}
placeholder={
canComment
? "Commente"
: "Connecte-toi pour laisser un commentaire"
}
placeholderTextColor="#FFFFFF90"
editable={canComment}
style={[styles.commentInput, !canComment && { opacity: 0.6 }]}
/> />
</Pressable> <Pressable
onPress={onSend}
disabled={!canComment || !text.trim()}
style={({ pressed }) => [
styles.sendButton,
(!canComment || !text.trim()) && styles.sendButtonDisabled,
pressed && canComment && styles.sendButtonPressed,
]}
>
<Image
source={icons.send}
style={styles.sendIcon}
resizeMode="contain"
/>
</Pressable>
</View>
</View> </View>
</BlurView> </BlurView>
); );
}; };
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const { width: viewportWidth } = useWindowDimensions(); const { width: viewportWidth, height: viewportHeight } =
const [layoutWidth, setLayoutWidth] = useState(viewportWidth); useWindowDimensions();
const [layoutSize, setLayoutSize] = useState({
width: viewportWidth,
height: viewportHeight,
});
const commentInputRef = useRef(null); const commentInputRef = useRef(null);
const { currentUID, followUser, unfollowUser } = useUser() || {}; const { currentUID, followUser, unfollowUser } = useUser() || {};
const videoUrl = item?.playbackUrl || null; const videoUrl = item?.playbackUrl || null;
@@ -439,12 +454,16 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
item?.description || item?.title || "Description chanson"; item?.description || item?.title || "Description chanson";
const handleLayout = useCallback((event) => { const handleLayout = useCallback((event) => {
const nextWidth = event?.nativeEvent?.layout?.width; const { width = 0, height = 0 } = event?.nativeEvent?.layout || {};
if (typeof nextWidth === "number" && nextWidth > 0) { setLayoutSize((prev) => ({
setLayoutWidth(nextWidth); width: width > 0 ? width : prev.width,
} height: height > 0 ? height : prev.height,
}));
}, []); }, []);
const layoutWidth = layoutSize.width || viewportWidth;
const layoutHeight = layoutSize.height || viewportHeight;
const widePaddingThreshold = 900; const widePaddingThreshold = 900;
const horizontalPadding = layoutWidth >= widePaddingThreshold ? 64 : 32; const horizontalPadding = layoutWidth >= widePaddingThreshold ? 64 : 32;
const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0); const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0);
@@ -454,20 +473,42 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const minCommentsWidth = 260; const minCommentsWidth = 260;
const maxCommentsWidth = 420; const maxCommentsWidth = 420;
let sideBySide = innerWidth >= 620; const baseInnerWidth = innerWidth > 0 ? innerWidth : viewportWidth;
let desiredHeight = Math.max(360, layoutHeight * 0.8);
let desiredWidth = desiredHeight * (9 / 16);
if (desiredWidth > maxVideoWidth) {
desiredWidth = maxVideoWidth;
desiredHeight = desiredWidth * (16 / 9);
}
if (desiredWidth < minVideoWidth) {
desiredWidth = minVideoWidth;
desiredHeight = desiredWidth * (16 / 9);
}
let sideBySide =
innerWidth >= desiredWidth + minCommentsWidth + gapBetweenColumns;
let videoWidth = sideBySide let videoWidth = sideBySide
? Math.min(maxVideoWidth, Math.max(innerWidth * 0.56, minVideoWidth)) ? Math.min(maxVideoWidth, Math.max(desiredWidth, minVideoWidth))
: Math.min(innerWidth, maxVideoWidth); : Math.min(Math.max(baseInnerWidth * 0.88, minVideoWidth), maxVideoWidth);
let videoHeight = videoWidth * (16 / 9);
if (videoHeight > desiredHeight) {
videoHeight = desiredHeight;
videoWidth = videoHeight * (9 / 16);
}
let commentsWidth = sideBySide let commentsWidth = sideBySide
? innerWidth - videoWidth - gapBetweenColumns ? innerWidth - videoWidth - gapBetweenColumns
: innerWidth; : baseInnerWidth;
if (sideBySide && commentsWidth > maxCommentsWidth) { if (sideBySide && commentsWidth > maxCommentsWidth) {
videoWidth = Math.min( const excess = commentsWidth - maxCommentsWidth;
maxVideoWidth,
videoWidth + (commentsWidth - maxCommentsWidth)
);
commentsWidth = maxCommentsWidth; commentsWidth = maxCommentsWidth;
videoWidth = Math.min(maxVideoWidth, videoWidth + excess);
videoHeight = videoWidth * (16 / 9);
} }
if (sideBySide && commentsWidth < minCommentsWidth) { if (sideBySide && commentsWidth < minCommentsWidth) {
@@ -475,33 +516,28 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
if (videoWidth - deficit >= minVideoWidth) { if (videoWidth - deficit >= minVideoWidth) {
videoWidth -= deficit; videoWidth -= deficit;
commentsWidth = minCommentsWidth; commentsWidth = minCommentsWidth;
videoHeight = videoWidth * (16 / 9);
} else { } else {
sideBySide = false; sideBySide = false;
videoWidth = Math.min(innerWidth, maxVideoWidth); videoWidth = Math.min(baseInnerWidth, maxVideoWidth);
commentsWidth = innerWidth; commentsWidth = baseInnerWidth;
videoHeight = videoWidth * (16 / 9);
} }
} }
const baseInnerWidth = innerWidth > 0 ? innerWidth : viewportWidth; if (!sideBySide) {
const safeVideoWidth = Math.max( videoHeight = Math.min(videoHeight, desiredHeight);
minVideoWidth, videoWidth = videoHeight * (9 / 16);
Math.min( commentsWidth = videoWidth;
Number.isFinite(videoWidth) ? videoWidth : baseInnerWidth, }
maxVideoWidth
) const panelHeight = Math.min(videoHeight, desiredHeight);
);
const videoCardWidth = sideBySide const containerHeight = viewportHeight;
? safeVideoWidth const verticalPadding = Math.max((layoutHeight - panelHeight) / 2, 24);
: Math.min(Math.max(baseInnerWidth * 0.88, minVideoWidth), maxVideoWidth);
const safeCommentsWidth = sideBySide const safeCommentsWidth = sideBySide
? Math.max( ? Math.max(minCommentsWidth, Math.min(commentsWidth, maxCommentsWidth))
minCommentsWidth, : undefined;
Math.min(
Number.isFinite(commentsWidth) ? commentsWidth : baseInnerWidth,
maxCommentsWidth
)
)
: 0;
return ( return (
<View <View
@@ -510,8 +546,9 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
styles.itemContainerBase, styles.itemContainerBase,
sideBySide ? styles.itemContainerRow : styles.itemContainerCompact, sideBySide ? styles.itemContainerRow : styles.itemContainerCompact,
{ {
height: responsiveHeight(70), height: "90%",
paddingHorizontal: sideBySide ? horizontalPadding : 24, paddingHorizontal: sideBySide ? horizontalPadding : 24,
paddingVertical: verticalPadding,
}, },
]} ]}
> >
@@ -519,11 +556,19 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
style={[ style={[
styles.videoColumn, styles.videoColumn,
sideBySide sideBySide
? { width: safeVideoWidth, marginRight: gapBetweenColumns } ? { width: videoWidth, marginRight: gapBetweenColumns }
: { width: "100%" }, : { width: "100%" },
]} ]}
> >
<View style={[styles.videoSurface, { width: videoCardWidth }]}> <View
style={[
styles.videoSurface,
{
// width: videoWidth,
height: panelHeight,
},
]}
>
<View style={styles.videoContainer}> <View style={styles.videoContainer}>
{!!videoUrl ? ( {!!videoUrl ? (
<VideoView <VideoView
@@ -689,8 +734,8 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
style={[ style={[
styles.commentsColumn, styles.commentsColumn,
sideBySide sideBySide
? { width: safeCommentsWidth } ? { width: safeCommentsWidth, height: panelHeight }
: styles.commentsColumnCompact, : [styles.commentsColumnCompact, { height: panelHeight }],
]} ]}
> >
<CommentsPanel <CommentsPanel
@@ -701,6 +746,7 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
setCommentsCount((c) => Math.max(0, Number(c || 0) + 1)) setCommentsCount((c) => Math.max(0, Number(c || 0) + 1))
} }
inputRef={commentInputRef} inputRef={commentInputRef}
panelHeight={panelHeight}
/> />
</View> </View>
</View> </View>
@@ -726,12 +772,10 @@ const Playbacks = () => {
}); });
useEffect(() => { useEffect(() => {
if (Platform.OS === "web") { if (Platform.OS !== "web") return undefined;
setGlobal({ webLayoutMode: "playbacks-wide" }); setGlobal({ webLayoutMode: isFocused ? "playbacks-wide" : "default" });
return () => setGlobal({ webLayoutMode: "default" }); return () => setGlobal({ webLayoutMode: "default" });
} }, [isFocused]);
return undefined;
}, []);
const onSnap = useCallback( const onSnap = useCallback(
(index) => { (index) => {
@@ -790,11 +834,9 @@ export default Playbacks;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
// backgroundColor: Palette.darkPurple,
}, },
itemContainerBase: { itemContainerBase: {
width: "100%", width: "100%",
// backgroundColor: Palette.darkPurple,
paddingVertical: 36, paddingVertical: 36,
alignItems: "center", alignItems: "center",
}, },
@@ -814,8 +856,8 @@ const styles = StyleSheet.create({
}, },
videoSurface: { videoSurface: {
alignSelf: "center", alignSelf: "center",
width: "100%", // width: 100,
maxWidth: 620, // maxWidth: 400,
aspectRatio: 9 / 16, aspectRatio: 9 / 16,
borderRadius: 32, borderRadius: 32,
overflow: "hidden", overflow: "hidden",
@@ -915,8 +957,16 @@ const styles = StyleSheet.create({
backgroundColor: Palette.glass, backgroundColor: Palette.glass,
minHeight: 0, minHeight: 0,
}, },
commentsInner: {
flex: 1,
justifyContent: "space-between",
},
commentsScroll: {
flex: 1,
},
commentsContent: { commentsContent: {
paddingBottom: 24, paddingBottom: 24,
flexGrow: 1,
}, },
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 18,