adaptive sizing for playercontrols,seriescontent,and commentsection

This commit is contained in:
tapframe 2025-10-19 20:30:24 +05:30
parent 0b764412b2
commit 175d47f71f
4 changed files with 202 additions and 85 deletions

View file

@ -837,41 +837,66 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
const renderSkeletons = useCallback(() => {
const placeholders = [0, 1, 2];
// Responsive skeleton sizes to match CompactCommentCard
const skWidth = isTV ? 360 : isLargeTablet ? 320 : isTablet ? 300 : 280;
const skHeight = isTV ? 200 : isLargeTablet ? 185 : isTablet ? 175 : 170;
const skPad = isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12;
const gap = isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12;
const headLineWidth = isTV ? 160 : isLargeTablet ? 140 : isTablet ? 130 : 120;
const ratingWidth = isTV ? 100 : isLargeTablet ? 90 : isTablet ? 85 : 80;
const statWidth = isTV ? 44 : isLargeTablet ? 40 : isTablet ? 38 : 36;
const badgeW = isTV ? 60 : isLargeTablet ? 56 : isTablet ? 52 : 50;
const badgeH = isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12;
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalList}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={[styles.horizontalList, { paddingRight: gap }]}>
{placeholders.map((i) => (
<View key={`skeleton-${i}`} style={[styles.compactCard, { backgroundColor: currentTheme.colors.card, borderColor: currentTheme.colors.border }]}>
<View
key={`skeleton-${i}`}
style={[
styles.compactCard,
{
backgroundColor: currentTheme.colors.card,
borderColor: currentTheme.colors.border,
width: skWidth,
height: skHeight,
marginRight: gap,
padding: skPad,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
},
]}
>
<View style={styles.skeletonTraktContainer}>
<View style={[styles.skeletonDot]} />
<View style={[styles.skeletonDot, { width: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16, height: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16, borderRadius: isTV ? 10 : isLargeTablet ? 9 : 8 }]} />
</View>
<View style={styles.compactHeader}>
<View style={[styles.skeletonLine, { width: 120 }]} />
<View style={[styles.miniVipBadge, styles.skeletonBadge]} />
<View style={[styles.compactHeader, { marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8 }]}>
<View style={[styles.skeletonLine, { width: headLineWidth, height: isTV ? 14 : 12 }]} />
<View style={[styles.miniVipBadge, styles.skeletonBadge, { width: isTV ? 36 : isLargeTablet ? 32 : isTablet ? 28 : 24, height: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12, borderRadius: isTV ? 10 : isLargeTablet ? 9 : 8 }]} />
</View>
<View style={styles.compactRating}>
<View style={[styles.skeletonLine, { width: 80, height: 10 }]} />
<View style={[styles.compactRating, { marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8 }]}>
<View style={[styles.skeletonLine, { width: ratingWidth, height: isTV ? 12 : 10 }]} />
</View>
<View style={styles.commentContainer}>
<View style={[styles.skeletonLine, { width: '95%' }]} />
<View style={[styles.skeletonLine, { width: '90%', marginTop: 6 }]} />
<View style={[styles.skeletonLine, { width: '70%', marginTop: 6 }]} />
<View style={[styles.commentContainer, { marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8 }]}>
<View style={[styles.skeletonLine, { width: '95%', height: isTV ? 14 : 12 }]} />
<View style={[styles.skeletonLine, { width: '90%', height: isTV ? 14 : 12, marginTop: 6 }]} />
<View style={[styles.skeletonLine, { width: '70%', height: isTV ? 14 : 12, marginTop: 6 }]} />
</View>
<View style={styles.compactMeta}>
<View style={[styles.skeletonBadge, { width: 50, height: 12, borderRadius: 6 }]} />
<View style={{ flexDirection: 'row', gap: 8 }}>
<View style={[styles.skeletonLine, { width: 36, height: 10 }]} />
<View style={[styles.skeletonLine, { width: 36, height: 10 }]} />
<View style={[styles.compactMeta, { paddingTop: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 6 : 6 }]}>
<View style={[styles.skeletonBadge, { width: badgeW, height: badgeH, borderRadius: Math.min(6, badgeH / 2) }]} />
<View style={{ flexDirection: 'row', gap }}>
<View style={[styles.skeletonLine, { width: statWidth, height: isTV ? 12 : 10 }]} />
<View style={[styles.skeletonLine, { width: statWidth, height: isTV ? 12 : 10 }]} />
</View>
</View>
</View>
))}
</ScrollView>
);
}, [currentTheme]);
}, [currentTheme, isTV, isLargeTablet, isTablet]);
// Don't show section if not authenticated, if comments are disabled in settings, or if still checking authentication
// Only show when authentication is definitively true and settings allow it

View file

@ -83,6 +83,46 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
return 16; // phone
}
}, [deviceType]);
// Match ThisWeekSection card sizing for horizontal episode cards
const horizontalCardWidth = useMemo(() => {
switch (deviceType) {
case 'tv':
return Math.min(deviceWidth * 0.25, 400);
case 'largeTablet':
return Math.min(deviceWidth * 0.35, 350);
case 'tablet':
return Math.min(deviceWidth * 0.46, 300);
default:
return width * 0.75;
}
}, [deviceType, deviceWidth, width]);
const horizontalCardHeight = useMemo(() => {
switch (deviceType) {
case 'tv':
return 280;
case 'largeTablet':
return 250;
case 'tablet':
return 220;
default:
return 180;
}
}, [deviceType]);
const horizontalItemSpacing = useMemo(() => {
switch (deviceType) {
case 'tv':
return 20;
case 'largeTablet':
return 18;
case 'tablet':
return 16;
default:
return 16;
}
}, [deviceType]);
// Enhanced season poster sizing
const seasonPosterWidth = useMemo(() => {
@ -730,6 +770,20 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
<MaterialIcons name="check" size={isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12} color={currentTheme.colors.white} />
</View>
)}
{(!progress || progressPercent === 0) && (
<View style={{
position: 'absolute',
top: 8,
left: 8,
width: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
height: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
borderRadius: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 10 : 10,
borderWidth: 2,
borderStyle: 'dashed',
borderColor: currentTheme.colors.textMuted,
opacity: 0.85,
}} />
)}
</View>
<View style={[
@ -874,7 +928,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
styles.episodeCardHorizontal,
{
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
height: isTV ? 280 : isLargeTablet ? 260 : isTablet ? 240 : 200,
height: horizontalCardHeight,
elevation: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8,
shadowOpacity: isTV ? 0.4 : isLargeTablet ? 0.35 : isTablet ? 0.3 : 0.3,
shadowRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8
@ -882,7 +936,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
// Gradient border styling
{
borderWidth: 1,
borderColor: 'transparent',
borderColor: 'rgba(255,255,255,0.12)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
}
@ -890,32 +944,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
onPress={() => onSelectEpisode(episode)}
activeOpacity={0.85}
>
{/* Gradient Border Container */}
<View style={{
position: 'absolute',
top: -1,
left: -1,
right: -1,
bottom: -1,
borderRadius: 17,
zIndex: -1,
}}>
<LinearGradient
colors={[
'#ffffff80', // White with 50% opacity
'#ffffff40', // White with 25% opacity
'#ffffff20', // White with 12% opacity
'#ffffff40', // White with 25% opacity
'#ffffff80', // White with 50% opacity
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
flex: 1,
borderRadius: 17,
}}
/>
</View>
{/* Solid outline replaces gradient border */}
{/* Background Image */}
<FastImage
@ -1057,6 +1086,20 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
<MaterialIcons name="check" size={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16} color="#fff" />
</View>
)}
{(!progress || progressPercent === 0) && (
<View style={{
position: 'absolute',
top: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
left: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
width: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 24,
height: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 24,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
borderWidth: 2,
borderStyle: 'dashed',
borderColor: currentTheme.colors.textMuted,
opacity: 0.9,
}} />
)}
</LinearGradient>
</TouchableOpacity>
@ -1115,8 +1158,8 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
style={[
styles.episodeCardWrapperHorizontal,
{
width: isTV ? width * 0.45 : isLargeTablet ? width * 0.4 : isTablet ? width * 0.4 : width * 0.75,
marginRight: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 20 : 16
width: horizontalCardWidth,
marginRight: horizontalItemSpacing
}
]}
>
@ -1138,11 +1181,10 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
maxToRenderPerBatch={5}
windowSize={5}
getItemLayout={(data, index) => {
const cardWidth = isTV ? width * 0.45 : isLargeTablet ? width * 0.4 : isTablet ? width * 0.4 : width * 0.75;
const margin = isTV ? 24 : isLargeTablet ? 20 : isTablet ? 20 : 16;
const length = horizontalCardWidth + horizontalItemSpacing;
return {
length: cardWidth + margin,
offset: (cardWidth + margin) * index,
length,
offset: length * index,
index,
};
}}

View file

@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform } from 'react-native';
import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform, Dimensions } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Slider from '@react-native-community/slider';
@ -82,6 +82,22 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
playerBackend,
}) => {
const { currentTheme } = useTheme();
const deviceWidth = Dimensions.get('window').width;
const BREAKPOINTS = { phone: 0, tablet: 768, largeTablet: 1024, tv: 1440 } as const;
const getDeviceType = (w: number) => {
if (w >= BREAKPOINTS.tv) return 'tv';
if (w >= BREAKPOINTS.largeTablet) return 'largeTablet';
if (w >= BREAKPOINTS.tablet) return 'tablet';
return 'phone';
};
const deviceType = getDeviceType(deviceWidth);
const isTablet = deviceType === 'tablet';
const isLargeTablet = deviceType === 'largeTablet';
const isTV = deviceType === 'tv';
const closeIconSize = isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24;
const skipIconSize = isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24;
const playIconSize = isTV ? 56 : isLargeTablet ? 48 : isTablet ? 44 : 40;
return (
<Animated.View
style={[StyleSheet.absoluteFill, { opacity: fadeAnim, zIndex: 20 }]}
@ -141,7 +157,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
)}
</View>
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<Ionicons name="close" size={24} color="white" />
<Ionicons name="close" size={closeIconSize} color="white" />
</TouchableOpacity>
</View>
</LinearGradient>
@ -149,14 +165,14 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
{/* Center Controls (Play/Pause, Skip) */}
<View style={styles.controls}>
<TouchableOpacity onPress={() => skip(-10)} style={styles.skipButton}>
<Ionicons name="play-back" size={24} color="white" />
<Ionicons name="play-back" size={skipIconSize} color="white" />
<Text style={styles.skipText}>10</Text>
</TouchableOpacity>
<TouchableOpacity onPress={togglePlayback} style={styles.playButton}>
<Ionicons name={paused ? "play" : "pause"} size={40} color="white" />
<Ionicons name={paused ? "play" : "pause"} size={playIconSize} color="white" />
</TouchableOpacity>
<TouchableOpacity onPress={() => skip(10)} style={styles.skipButton}>
<Ionicons name="play-forward" size={24} color="white" />
<Ionicons name="play-forward" size={skipIconSize} color="white" />
<Text style={styles.skipText}>10</Text>
</TouchableOpacity>
</View>

View file

@ -1,4 +1,38 @@
import { StyleSheet } from 'react-native';
import { StyleSheet, Dimensions } from 'react-native';
const deviceWidth = Dimensions.get('window').width;
const BREAKPOINTS = { phone: 0, tablet: 768, largeTablet: 1024, tv: 1440 } as const;
const getDeviceType = (w: number) => {
if (w >= BREAKPOINTS.tv) return 'tv';
if (w >= BREAKPOINTS.largeTablet) return 'largeTablet';
if (w >= BREAKPOINTS.tablet) return 'tablet';
return 'phone';
};
const deviceType = getDeviceType(deviceWidth);
const isTablet = deviceType === 'tablet';
const isLargeTablet = deviceType === 'largeTablet';
const isTV = deviceType === 'tv';
// Scales for larger displays
const padH = isTV ? 28 : isLargeTablet ? 24 : isTablet ? 20 : 20;
const padV = isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16;
const titleFont = isTV ? 28 : isLargeTablet ? 24 : isTablet ? 22 : 18;
const episodeInfoFont = isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14;
const metadataFont = isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12;
const qualityPadH = isTV ? 10 : isLargeTablet ? 9 : isTablet ? 8 : 8;
const qualityPadV = isTV ? 4 : isLargeTablet ? 3 : isTablet ? 3 : 2;
const qualityRadius = isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4;
const qualityTextFont = isTV ? 13 : isLargeTablet ? 12 : isTablet ? 11 : 11;
const controlsGap = isTV ? 56 : isLargeTablet ? 48 : isTablet ? 44 : 40;
const controlsTranslateY = isTV ? -48 : isLargeTablet ? -42 : isTablet ? -36 : -30;
const skipTextFont = isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12;
const sliderBottom = isTV ? 80 : isLargeTablet ? 70 : isTablet ? 65 : 55;
const progressTouchHeight = isTV ? 48 : isLargeTablet ? 44 : isTablet ? 40 : 40;
const progressBarHeight = isTV ? 6 : isLargeTablet ? 5 : isTablet ? 5 : 4;
const progressThumbSize = isTV ? 24 : isLargeTablet ? 20 : isTablet ? 18 : 16;
const progressThumbTop = isTV ? -10 : isLargeTablet ? -8 : isTablet ? -7 : -6;
const durationFont = isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12;
const bottomButtonTextFont = isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12;
export const styles = StyleSheet.create({
container: {
@ -37,14 +71,14 @@ export const styles = StyleSheet.create({
padding: 0,
},
topGradient: {
paddingTop: 20,
paddingHorizontal: 20,
paddingBottom: 10,
paddingTop: padV,
paddingHorizontal: padH,
paddingBottom: Math.max(10, Math.round(padV * 0.6)),
},
bottomGradient: {
paddingBottom: 20,
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: padV,
paddingHorizontal: padH,
paddingTop: padV,
},
header: {
flexDirection: 'row',
@ -57,12 +91,12 @@ export const styles = StyleSheet.create({
},
title: {
color: 'white',
fontSize: 18,
fontSize: titleFont,
fontWeight: 'bold',
},
episodeInfo: {
color: 'rgba(255, 255, 255, 0.9)',
fontSize: 14,
fontSize: episodeInfoFont,
marginTop: 3,
},
metadataRow: {
@ -73,20 +107,20 @@ export const styles = StyleSheet.create({
},
metadataText: {
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 12,
fontSize: metadataFont,
marginRight: 8,
},
qualityBadge: {
backgroundColor: 'rgba(229, 9, 20, 0.2)',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 4,
paddingHorizontal: qualityPadH,
paddingVertical: qualityPadV,
borderRadius: qualityRadius,
marginRight: 8,
marginBottom: 4,
},
qualityText: {
color: '#E50914',
fontSize: 11,
fontSize: qualityTextFont,
fontWeight: 'bold',
},
providerText: {
@ -102,11 +136,11 @@ export const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 40,
gap: controlsGap,
left: 0,
right: 0,
top: '50%',
transform: [{ translateY: -30 }],
transform: [{ translateY: controlsTranslateY }],
zIndex: 1000,
},
playButton: {
@ -120,7 +154,7 @@ export const styles = StyleSheet.create({
},
skipText: {
color: 'white',
fontSize: 12,
fontSize: skipTextFont,
marginTop: 2,
},
bottomControls: {
@ -128,19 +162,19 @@ export const styles = StyleSheet.create({
},
sliderContainer: {
position: 'absolute',
bottom: 55,
bottom: sliderBottom,
left: 0,
right: 0,
paddingHorizontal: 20,
paddingHorizontal: padH,
zIndex: 1000,
},
progressTouchArea: {
height: 40, // Increased from 30 to give more space for the thumb
height: progressTouchHeight, // Increased touch area for larger displays
justifyContent: 'center',
width: '100%',
},
progressBarContainer: {
height: 4,
height: progressBarHeight,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 2,
overflow: 'hidden',
@ -164,12 +198,12 @@ export const styles = StyleSheet.create({
},
progressThumb: {
position: 'absolute',
width: 16,
height: 16,
borderRadius: 8,
width: progressThumbSize,
height: progressThumbSize,
borderRadius: progressThumbSize / 2,
backgroundColor: '#E50914',
top: -6, // Position to center on the progress bar
marginLeft: -8, // Center the thumb horizontally
top: progressThumbTop, // Position to center on the progress bar
marginLeft: -(progressThumbSize / 2), // Center the thumb horizontally
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
@ -187,7 +221,7 @@ export const styles = StyleSheet.create({
},
duration: {
color: 'white',
fontSize: 12,
fontSize: durationFont,
fontWeight: '500',
},
bottomButtons: {
@ -202,7 +236,7 @@ export const styles = StyleSheet.create({
},
bottomButtonText: {
color: 'white',
fontSize: 12,
fontSize: bottomButtonTextFont,
},
modalOverlay: {
flex: 1,