mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-18 07:12:18 +00:00
metascreen abruption fix
This commit is contained in:
parent
2e61617f83
commit
51064a65b2
1 changed files with 102 additions and 69 deletions
|
|
@ -59,7 +59,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
// Enhanced responsive sizing for tablets and TV screens
|
||||
const deviceWidth = Dimensions.get('window').width;
|
||||
const deviceHeight = Dimensions.get('window').height;
|
||||
|
||||
|
||||
// Determine device type based on width
|
||||
const getDeviceType = useCallback(() => {
|
||||
if (deviceWidth >= BREAKPOINTS.tv) return 'tv';
|
||||
|
|
@ -67,13 +67,13 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet';
|
||||
return 'phone';
|
||||
}, [deviceWidth]);
|
||||
|
||||
|
||||
const deviceType = getDeviceType();
|
||||
const isTablet = deviceType === 'tablet';
|
||||
const isLargeTablet = deviceType === 'largeTablet';
|
||||
const isTV = deviceType === 'tv';
|
||||
const isLargeScreen = isTablet || isLargeTablet || isTV;
|
||||
|
||||
|
||||
// Enhanced spacing and padding
|
||||
const horizontalPadding = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
|
|
@ -89,8 +89,11 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
}, [deviceType]);
|
||||
|
||||
// Animation values for smooth height transition
|
||||
const animatedHeight = useSharedValue(0);
|
||||
// Start with a reasonable default height (3 lines * 24px line height = 72px) to prevent layout shift
|
||||
const defaultCollapsedHeight = isTV ? 84 : isLargeTablet ? 78 : isTablet ? 72 : 72;
|
||||
const animatedHeight = useSharedValue(defaultCollapsedHeight);
|
||||
const [measuredHeights, setMeasuredHeights] = useState({ collapsed: 0, expanded: 0 });
|
||||
const [hasInitialMeasurement, setHasInitialMeasurement] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMDBListEnabled = async () => {
|
||||
|
|
@ -101,7 +104,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
setIsMDBEnabled(false); // Default to disabled if there's an error
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
checkMDBListEnabled();
|
||||
}, []);
|
||||
|
||||
|
|
@ -114,6 +117,12 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
const handleCollapsedTextLayout = (event: any) => {
|
||||
const { height } = event.nativeEvent.layout;
|
||||
setMeasuredHeights(prev => ({ ...prev, collapsed: height }));
|
||||
// Only set initial measurement flag once we have a valid height
|
||||
if (height > 0 && !hasInitialMeasurement) {
|
||||
setHasInitialMeasurement(true);
|
||||
// Update animated height immediately without animation for first measurement
|
||||
animatedHeight.value = height;
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandedTextLayout = (event: any) => {
|
||||
|
|
@ -128,49 +137,53 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
|
|||
setIsFullDescriptionOpen(!isFullDescriptionOpen);
|
||||
};
|
||||
|
||||
// Initialize height when component mounts or text changes
|
||||
// Update height when measurements change (only after initial measurement)
|
||||
useEffect(() => {
|
||||
if (measuredHeights.collapsed > 0) {
|
||||
animatedHeight.value = measuredHeights.collapsed;
|
||||
if (measuredHeights.collapsed > 0 && hasInitialMeasurement && !isFullDescriptionOpen) {
|
||||
// Only animate if the height actually changed significantly
|
||||
const currentHeight = animatedHeight.value;
|
||||
if (Math.abs(currentHeight - measuredHeights.collapsed) > 5) {
|
||||
animatedHeight.value = measuredHeights.collapsed;
|
||||
}
|
||||
}
|
||||
}, [measuredHeights.collapsed]);
|
||||
}, [measuredHeights.collapsed, hasInitialMeasurement, isFullDescriptionOpen]);
|
||||
|
||||
// Animated style for smooth height transition
|
||||
// Animated style for smooth height transition - use minHeight to prevent collapse to 0
|
||||
const animatedDescriptionStyle = useAnimatedStyle(() => ({
|
||||
height: animatedHeight.value,
|
||||
height: animatedHeight.value > 0 ? animatedHeight.value : defaultCollapsedHeight,
|
||||
overflow: 'hidden',
|
||||
}));
|
||||
|
||||
function formatRuntime(runtime: string): string {
|
||||
// Try to match formats like "1h55min", "2h 7min", "125 min", etc.
|
||||
const match = runtime.match(/(?:(\d+)\s*h\s*)?(\d+)\s*min/i);
|
||||
if (match) {
|
||||
const h = match[1] ? parseInt(match[1], 10) : 0;
|
||||
const m = match[2] ? parseInt(match[2], 10) : 0;
|
||||
if (h > 0) {
|
||||
return `${h}H ${m}M`;
|
||||
function formatRuntime(runtime: string): string {
|
||||
// Try to match formats like "1h55min", "2h 7min", "125 min", etc.
|
||||
const match = runtime.match(/(?:(\d+)\s*h\s*)?(\d+)\s*min/i);
|
||||
if (match) {
|
||||
const h = match[1] ? parseInt(match[1], 10) : 0;
|
||||
const m = match[2] ? parseInt(match[2], 10) : 0;
|
||||
if (h > 0) {
|
||||
return `${h}H ${m}M`;
|
||||
}
|
||||
if (m < 60) {
|
||||
return `${m} MIN`;
|
||||
}
|
||||
const hours = Math.floor(m / 60);
|
||||
const mins = m % 60;
|
||||
return hours > 0 ? `${hours}H ${mins}M` : `${mins} MIN`;
|
||||
}
|
||||
if (m < 60) {
|
||||
return `${m} MIN`;
|
||||
|
||||
// Fallback: treat as minutes if it's a number
|
||||
const r = parseInt(runtime, 10);
|
||||
if (!isNaN(r)) {
|
||||
if (r < 60) return `${r} MIN`;
|
||||
const h = Math.floor(r / 60);
|
||||
const m = r % 60;
|
||||
return h > 0 ? `${h}H ${m}M` : `${m} MIN`;
|
||||
}
|
||||
const hours = Math.floor(m / 60);
|
||||
const mins = m % 60;
|
||||
return hours > 0 ? `${hours}H ${mins}M` : `${mins} MIN`;
|
||||
}
|
||||
|
||||
// Fallback: treat as minutes if it's a number
|
||||
const r = parseInt(runtime, 10);
|
||||
if (!isNaN(r)) {
|
||||
if (r < 60) return `${r} MIN`;
|
||||
const h = Math.floor(r / 60);
|
||||
const m = r % 60;
|
||||
return h > 0 ? `${h}H ${m}M` : `${m} MIN`;
|
||||
}
|
||||
// If not matched, return as is
|
||||
return runtime;
|
||||
|
||||
// If not matched, return as is
|
||||
return runtime;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -188,17 +201,17 @@ function formatRuntime(runtime: string): string {
|
|||
|
||||
{/* Meta Info */}
|
||||
<View style={[
|
||||
styles.metaInfo,
|
||||
styles.metaInfo,
|
||||
loadingMetadata && styles.dimmed,
|
||||
{
|
||||
{
|
||||
paddingHorizontal: horizontalPadding,
|
||||
gap: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18
|
||||
}
|
||||
]}>
|
||||
{metadata.year && (
|
||||
<Text style={[
|
||||
styles.metaText,
|
||||
{
|
||||
styles.metaText,
|
||||
{
|
||||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
|
||||
}
|
||||
|
|
@ -206,8 +219,8 @@ function formatRuntime(runtime: string): string {
|
|||
)}
|
||||
{metadata.runtime && (
|
||||
<Text style={[
|
||||
styles.metaText,
|
||||
{
|
||||
styles.metaText,
|
||||
{
|
||||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
|
||||
}
|
||||
|
|
@ -232,8 +245,8 @@ function formatRuntime(runtime: string): string {
|
|||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.ratingText,
|
||||
{
|
||||
styles.ratingText,
|
||||
{
|
||||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
|
||||
}
|
||||
|
|
@ -249,7 +262,7 @@ function formatRuntime(runtime: string): string {
|
|||
<Animated.View
|
||||
entering={FadeIn.duration(300).delay(100)}
|
||||
style={[
|
||||
styles.creatorContainer,
|
||||
styles.creatorContainer,
|
||||
loadingMetadata && styles.dimmed,
|
||||
{ paddingHorizontal: horizontalPadding }
|
||||
]}
|
||||
|
|
@ -263,16 +276,16 @@ function formatRuntime(runtime: string): string {
|
|||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.creatorLabel,
|
||||
{
|
||||
styles.creatorLabel,
|
||||
{
|
||||
color: currentTheme.colors.white,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
|
||||
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
|
||||
}
|
||||
]}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
|
||||
<Text style={[
|
||||
styles.creatorText,
|
||||
{
|
||||
styles.creatorText,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
|
||||
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
|
||||
|
|
@ -289,16 +302,16 @@ function formatRuntime(runtime: string): string {
|
|||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.creatorLabel,
|
||||
{
|
||||
styles.creatorLabel,
|
||||
{
|
||||
color: currentTheme.colors.white,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
|
||||
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
|
||||
}
|
||||
]}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
|
||||
<Text style={[
|
||||
styles.creatorText,
|
||||
{
|
||||
styles.creatorText,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
|
||||
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
|
||||
|
|
@ -308,11 +321,11 @@ function formatRuntime(runtime: string): string {
|
|||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* Description */}
|
||||
{metadata.description && (
|
||||
{/* Description - Show skeleton if no description yet to prevent layout shift */}
|
||||
{metadata.description ? (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.descriptionContainer,
|
||||
styles.descriptionContainer,
|
||||
loadingMetadata && styles.dimmed,
|
||||
{ paddingHorizontal: horizontalPadding }
|
||||
]}
|
||||
|
|
@ -321,10 +334,10 @@ function formatRuntime(runtime: string): string {
|
|||
{/* Hidden text elements to measure heights */}
|
||||
<Text
|
||||
style={[
|
||||
styles.description,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
position: 'absolute',
|
||||
styles.description,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15,
|
||||
lineHeight: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24
|
||||
|
|
@ -337,10 +350,10 @@ function formatRuntime(runtime: string): string {
|
|||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.description,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
position: 'absolute',
|
||||
styles.description,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15,
|
||||
lineHeight: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24
|
||||
|
|
@ -359,8 +372,8 @@ function formatRuntime(runtime: string): string {
|
|||
<Animated.View style={animatedDescriptionStyle}>
|
||||
<Text
|
||||
style={[
|
||||
styles.description,
|
||||
{
|
||||
styles.description,
|
||||
{
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15,
|
||||
lineHeight: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24
|
||||
|
|
@ -381,8 +394,8 @@ function formatRuntime(runtime: string): string {
|
|||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.showMoreText,
|
||||
{
|
||||
styles.showMoreText,
|
||||
{
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14
|
||||
}
|
||||
|
|
@ -398,6 +411,20 @@ function formatRuntime(runtime: string): string {
|
|||
)}
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
) : (
|
||||
/* Skeleton placeholder for description to prevent layout shift */
|
||||
<View
|
||||
style={[
|
||||
styles.descriptionContainer,
|
||||
{ paddingHorizontal: horizontalPadding, minHeight: defaultCollapsedHeight }
|
||||
]}
|
||||
>
|
||||
<View style={[styles.descriptionSkeleton, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<View style={[styles.skeletonLine, { width: '100%', height: isTV ? 18 : 15, backgroundColor: currentTheme.colors.elevation1, marginBottom: 8 }]} />
|
||||
<View style={[styles.skeletonLine, { width: '95%', height: isTV ? 18 : 15, backgroundColor: currentTheme.colors.elevation1, marginBottom: 8 }]} />
|
||||
<View style={[styles.skeletonLine, { width: '80%', height: isTV ? 18 : 15, backgroundColor: currentTheme.colors.elevation1 }]} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
@ -491,6 +518,12 @@ const styles = StyleSheet.create({
|
|||
fontSize: 14,
|
||||
marginRight: 4,
|
||||
},
|
||||
descriptionSkeleton: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
skeletonLine: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(MetadataDetails);
|
||||
Loading…
Reference in a new issue