mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-09 19:40:44 +00:00
discover screen init
This commit is contained in:
parent
a30fa604d7
commit
cf5cc2d8f9
6 changed files with 353 additions and 50 deletions
268
src/components/common/Poster.tsx
Normal file
268
src/components/common/Poster.tsx
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
Platform,
|
||||||
|
Dimensions,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native';
|
||||||
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
// Enhanced responsive breakpoints
|
||||||
|
const BREAKPOINTS = {
|
||||||
|
phone: 0,
|
||||||
|
tablet: 768,
|
||||||
|
largeTablet: 1024,
|
||||||
|
tv: 1440,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeviceType = (screenWidth: number) => {
|
||||||
|
if (screenWidth >= BREAKPOINTS.tv) return 'tv';
|
||||||
|
if (screenWidth >= BREAKPOINTS.largeTablet) return 'largeTablet';
|
||||||
|
if (screenWidth >= BREAKPOINTS.tablet) return 'tablet';
|
||||||
|
return 'phone';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PosterShape = 'poster' | 'landscape' | 'square';
|
||||||
|
|
||||||
|
export interface PosterProps {
|
||||||
|
/** The poster image URL */
|
||||||
|
uri?: string | null;
|
||||||
|
/** Width of the poster */
|
||||||
|
width: number;
|
||||||
|
/** Shape of the poster - determines aspect ratio */
|
||||||
|
shape?: PosterShape;
|
||||||
|
/** Optional custom aspect ratio override */
|
||||||
|
aspectRatio?: number;
|
||||||
|
/** Optional custom border radius (uses settings.posterBorderRadius by default) */
|
||||||
|
borderRadius?: number;
|
||||||
|
/** Optional title to display below the poster */
|
||||||
|
title?: string;
|
||||||
|
/** Whether to show the title */
|
||||||
|
showTitle?: boolean;
|
||||||
|
/** Fallback text to show when no poster is available */
|
||||||
|
fallbackText?: string;
|
||||||
|
/** Additional styles for the container */
|
||||||
|
style?: ViewStyle;
|
||||||
|
/** Additional styles for the poster container */
|
||||||
|
posterStyle?: ViewStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Poster component with consistent styling across the app.
|
||||||
|
* Matches the design from ContentItem.tsx with:
|
||||||
|
* - Border: 1.5px solid rgba(255,255,255,0.15)
|
||||||
|
* - Border Radius: settings.posterBorderRadius (default 12)
|
||||||
|
* - Shadow: elevation 1 on Android, subtle shadow on iOS
|
||||||
|
* - Aspect Ratio: 2/3 for poster, 16/9 for landscape, 1/1 for square
|
||||||
|
*/
|
||||||
|
export const Poster: React.FC<PosterProps> = ({
|
||||||
|
uri,
|
||||||
|
width: posterWidth,
|
||||||
|
shape = 'poster',
|
||||||
|
aspectRatio: customAspectRatio,
|
||||||
|
borderRadius: customBorderRadius,
|
||||||
|
title,
|
||||||
|
showTitle = false,
|
||||||
|
fallbackText,
|
||||||
|
style,
|
||||||
|
posterStyle,
|
||||||
|
}) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const { settings, isLoaded } = useSettings();
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
|
// Reset error state when URI changes
|
||||||
|
useEffect(() => {
|
||||||
|
setImageError(false);
|
||||||
|
}, [uri]);
|
||||||
|
|
||||||
|
// Determine aspect ratio based on shape
|
||||||
|
const aspectRatio = useMemo(() => {
|
||||||
|
if (customAspectRatio) return customAspectRatio;
|
||||||
|
switch (shape) {
|
||||||
|
case 'landscape':
|
||||||
|
return 16 / 9;
|
||||||
|
case 'square':
|
||||||
|
return 1;
|
||||||
|
case 'poster':
|
||||||
|
default:
|
||||||
|
return 2 / 3;
|
||||||
|
}
|
||||||
|
}, [shape, customAspectRatio]);
|
||||||
|
|
||||||
|
// Border radius from settings or custom
|
||||||
|
const borderRadius = customBorderRadius ??
|
||||||
|
(typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12);
|
||||||
|
|
||||||
|
// Device type for responsive title sizing
|
||||||
|
const deviceType = getDeviceType(width);
|
||||||
|
|
||||||
|
// Title font size based on device type
|
||||||
|
const titleFontSize = useMemo(() => {
|
||||||
|
switch (deviceType) {
|
||||||
|
case 'tv':
|
||||||
|
return 16;
|
||||||
|
case 'largeTablet':
|
||||||
|
return 15;
|
||||||
|
case 'tablet':
|
||||||
|
return 14;
|
||||||
|
default:
|
||||||
|
return 13;
|
||||||
|
}
|
||||||
|
}, [deviceType]);
|
||||||
|
|
||||||
|
// Optimize poster URL for TMDB
|
||||||
|
const optimizedUrl = useMemo(() => {
|
||||||
|
if (!uri || uri.includes('placeholder')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (uri.includes('image.tmdb.org')) {
|
||||||
|
return uri.replace(/\/w\d+\//, '/w154/');
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
}, [uri]);
|
||||||
|
|
||||||
|
// Placeholder while settings load
|
||||||
|
if (!isLoaded) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { width: posterWidth }, style]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.posterContainer,
|
||||||
|
{
|
||||||
|
width: posterWidth,
|
||||||
|
aspectRatio,
|
||||||
|
borderRadius,
|
||||||
|
backgroundColor: currentTheme.colors.elevation1,
|
||||||
|
},
|
||||||
|
posterStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{showTitle && <View style={{ height: 18, marginTop: 4 }} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { width: posterWidth }, style]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.posterContainer,
|
||||||
|
{
|
||||||
|
width: posterWidth,
|
||||||
|
aspectRatio,
|
||||||
|
borderRadius,
|
||||||
|
backgroundColor: currentTheme.colors.elevation1,
|
||||||
|
},
|
||||||
|
posterStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{optimizedUrl && !imageError ? (
|
||||||
|
<FastImage
|
||||||
|
source={{
|
||||||
|
uri: optimizedUrl,
|
||||||
|
priority: FastImage.priority.normal,
|
||||||
|
cache: FastImage.cacheControl.immutable,
|
||||||
|
}}
|
||||||
|
style={[styles.poster, { borderRadius }]}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
onLoad={() => setImageError(false)}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.poster,
|
||||||
|
styles.fallbackContainer,
|
||||||
|
{
|
||||||
|
backgroundColor: currentTheme.colors.elevation1,
|
||||||
|
borderRadius,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{imageError ? (
|
||||||
|
<MaterialIcons
|
||||||
|
name="broken-image"
|
||||||
|
size={24}
|
||||||
|
color={currentTheme.colors.textMuted}
|
||||||
|
/>
|
||||||
|
) : fallbackText ? (
|
||||||
|
<Text
|
||||||
|
style={[styles.fallbackText, { color: currentTheme.colors.textMuted }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{fallbackText.length > 20 ? `${fallbackText.substring(0, 20)}...` : fallbackText}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<MaterialIcons
|
||||||
|
name="image"
|
||||||
|
size={24}
|
||||||
|
color={currentTheme.colors.textMuted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{showTitle && title && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.title,
|
||||||
|
{
|
||||||
|
color: currentTheme.colors.mediumEmphasis,
|
||||||
|
fontSize: titleFontSize,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {},
|
||||||
|
posterContainer: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
// Consistent shadow/elevation matching ContentItem
|
||||||
|
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 1,
|
||||||
|
// Consistent border styling
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
poster: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
fallbackContainer: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
fallbackText: {
|
||||||
|
fontSize: 10,
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontWeight: '500',
|
||||||
|
marginTop: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Poster;
|
||||||
|
|
@ -1089,7 +1089,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
borderColor: currentTheme.colors.border,
|
borderColor: currentTheme.colors.border,
|
||||||
shadowColor: currentTheme.colors.black,
|
shadowColor: currentTheme.colors.black,
|
||||||
width: computedItemWidth,
|
width: computedItemWidth,
|
||||||
height: computedItemHeight
|
height: computedItemHeight,
|
||||||
|
borderRadius: settings.posterBorderRadius ?? 12,
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
|
|
@ -1110,7 +1111,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
priority: FastImage.priority.high,
|
priority: FastImage.priority.high,
|
||||||
cache: FastImage.cacheControl.immutable
|
cache: FastImage.cacheControl.immutable
|
||||||
}}
|
}}
|
||||||
style={styles.continueWatchingPoster}
|
style={[styles.continueWatchingPoster, { borderTopLeftRadius: settings.posterBorderRadius ?? 12, borderBottomLeftRadius: settings.posterBorderRadius ?? 12 }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -1348,13 +1349,15 @@ const styles = StyleSheet.create({
|
||||||
width: 280,
|
width: 280,
|
||||||
height: 120,
|
height: 120,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
borderRadius: 14,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
elevation: 6,
|
elevation: 1,
|
||||||
shadowOffset: { width: 0, height: 3 },
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.2,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowRadius: 6,
|
shadowOpacity: 0.05,
|
||||||
borderWidth: 1,
|
shadowRadius: 1,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
},
|
},
|
||||||
posterContainer: {
|
posterContainer: {
|
||||||
width: 80,
|
width: 80,
|
||||||
|
|
@ -1364,8 +1367,8 @@ const styles = StyleSheet.create({
|
||||||
continueWatchingPoster: {
|
continueWatchingPoster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderTopLeftRadius: 14,
|
borderTopLeftRadius: 12,
|
||||||
borderBottomLeftRadius: 14,
|
borderBottomLeftRadius: 12,
|
||||||
},
|
},
|
||||||
deletingOverlay: {
|
deletingOverlay: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -1451,26 +1454,28 @@ const styles = StyleSheet.create({
|
||||||
width: POSTER_WIDTH,
|
width: POSTER_WIDTH,
|
||||||
aspectRatio: 2 / 3,
|
aspectRatio: 2 / 3,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
elevation: 8,
|
elevation: 1,
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.3,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowRadius: 8,
|
shadowOpacity: 0.05,
|
||||||
borderWidth: 1,
|
shadowRadius: 1,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
},
|
},
|
||||||
contentItemContainer: {
|
contentItemContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
poster: {
|
poster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
},
|
},
|
||||||
episodeInfoContainer: {
|
episodeInfoContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||||
|
|
@ -14,6 +15,7 @@ import { NavigationProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
import { StreamingContent } from '../../services/catalogService';
|
import { StreamingContent } from '../../services/catalogService';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { TMDBService } from '../../services/tmdbService';
|
import { TMDBService } from '../../services/tmdbService';
|
||||||
import { catalogService } from '../../services/catalogService';
|
import { catalogService } from '../../services/catalogService';
|
||||||
import CustomAlert from '../../components/CustomAlert';
|
import CustomAlert from '../../components/CustomAlert';
|
||||||
|
|
@ -33,12 +35,14 @@ interface MoreLikeThisSectionProps {
|
||||||
loadingRecommendations: boolean;
|
loadingRecommendations: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||||
recommendations,
|
recommendations,
|
||||||
loadingRecommendations
|
loadingRecommendations
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
const { settings } = useSettings();
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const borderRadius = settings.posterBorderRadius ?? 12;
|
||||||
|
|
||||||
// Determine device type
|
// Determine device type
|
||||||
const deviceWidth = Dimensions.get('window').width;
|
const deviceWidth = Dimensions.get('window').width;
|
||||||
|
|
@ -91,16 +95,16 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||||
try {
|
try {
|
||||||
// Extract TMDB ID from the tmdb:123456 format
|
// Extract TMDB ID from the tmdb:123456 format
|
||||||
const tmdbId = item.id.replace('tmdb:', '');
|
const tmdbId = item.id.replace('tmdb:', '');
|
||||||
|
|
||||||
// Get Stremio ID directly using catalogService
|
// Get Stremio ID directly using catalogService
|
||||||
// The catalogService.getStremioId method already handles the conversion internally
|
// The catalogService.getStremioId method already handles the conversion internally
|
||||||
const stremioId = await catalogService.getStremioId(item.type, tmdbId);
|
const stremioId = await catalogService.getStremioId(item.type, tmdbId);
|
||||||
|
|
||||||
if (stremioId) {
|
if (stremioId) {
|
||||||
navigation.dispatch(
|
navigation.dispatch(
|
||||||
StackActions.push('Metadata', {
|
StackActions.push('Metadata', {
|
||||||
id: stremioId,
|
id: stremioId,
|
||||||
type: item.type
|
type: item.type
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -110,19 +114,19 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||||
if (__DEV__) console.error('Error navigating to recommendation:', error);
|
if (__DEV__) console.error('Error navigating to recommendation:', error);
|
||||||
setAlertTitle('Error');
|
setAlertTitle('Error');
|
||||||
setAlertMessage('Unable to load this content. Please try again later.');
|
setAlertMessage('Unable to load this content. Please try again later.');
|
||||||
setAlertActions([{ label: 'OK', onPress: () => {} }]);
|
setAlertActions([{ label: 'OK', onPress: () => { } }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: StreamingContent }) => (
|
const renderItem = ({ item }: { item: StreamingContent }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
|
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
|
||||||
onPress={() => handleItemPress(item)}
|
onPress={() => handleItemPress(item)}
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: item.poster }}
|
source={{ uri: item.poster }}
|
||||||
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, width: posterWidth, height: posterHeight, borderRadius: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 10 : 8 }]}
|
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, width: posterWidth, height: posterHeight, borderRadius }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13, lineHeight: isTV ? 20 : 18 }]} numberOfLines={2}>
|
<Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13, lineHeight: isTV ? 20 : 18 }]} numberOfLines={2}>
|
||||||
|
|
@ -144,7 +148,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { paddingLeft: 0 }] }>
|
<View style={[styles.container, { paddingLeft: 0 }]}>
|
||||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis, fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20, paddingHorizontal: horizontalPadding }]}>More Like This</Text>
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis, fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20, paddingHorizontal: horizontalPadding }]}>More Like This</Text>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={recommendations}
|
data={recommendations}
|
||||||
|
|
@ -183,10 +187,17 @@ const styles = StyleSheet.create({
|
||||||
marginRight: 12, // will be overridden responsively
|
marginRight: 12, // will be overridden responsively
|
||||||
},
|
},
|
||||||
poster: {
|
poster: {
|
||||||
borderRadius: 8, // overridden responsively
|
borderRadius: 12,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
borderWidth: 1,
|
// Consistent border styling matching ContentItem
|
||||||
|
borderWidth: 1.5,
|
||||||
borderColor: 'rgba(255,255,255,0.15)',
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
|
// Consistent shadow/elevation
|
||||||
|
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 1,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 13, // overridden responsively
|
fontSize: 13, // overridden responsively
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,10 @@ const DownloadItemComponent: React.FC<{
|
||||||
onRequestRemove: (item: DownloadItem) => void;
|
onRequestRemove: (item: DownloadItem) => void;
|
||||||
}> = React.memo(({ item, onPress, onAction, onRequestRemove }) => {
|
}> = React.memo(({ item, onPress, onAction, onRequestRemove }) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
const { settings } = useSettings();
|
||||||
const { showSuccess, showInfo } = useToast();
|
const { showSuccess, showInfo } = useToast();
|
||||||
const [posterUrl, setPosterUrl] = useState<string | null>(item.posterUrl || null);
|
const [posterUrl, setPosterUrl] = useState<string | null>(item.posterUrl || null);
|
||||||
|
const borderRadius = settings.posterBorderRadius ?? 12;
|
||||||
|
|
||||||
// Try to fetch poster if not available
|
// Try to fetch poster if not available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -212,10 +214,10 @@ const DownloadItemComponent: React.FC<{
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
{/* Poster */}
|
{/* Poster */}
|
||||||
<View style={styles.posterContainer}>
|
<View style={[styles.posterContainer, { borderRadius }]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: optimizePosterUrl(posterUrl) }}
|
source={{ uri: optimizePosterUrl(posterUrl) }}
|
||||||
style={styles.poster}
|
style={[styles.poster, { borderRadius }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
{/* Status indicator overlay */}
|
{/* Status indicator overlay */}
|
||||||
|
|
@ -790,16 +792,25 @@ const styles = StyleSheet.create({
|
||||||
posterContainer: {
|
posterContainer: {
|
||||||
width: POSTER_WIDTH,
|
width: POSTER_WIDTH,
|
||||||
height: POSTER_HEIGHT,
|
height: POSTER_HEIGHT,
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
marginRight: isTablet ? 20 : 16,
|
marginRight: isTablet ? 20 : 16,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: '#333',
|
backgroundColor: '#333',
|
||||||
|
// Consistent border styling matching ContentItem
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
|
// Consistent shadow/elevation
|
||||||
|
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 1,
|
||||||
},
|
},
|
||||||
poster: {
|
poster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
},
|
},
|
||||||
statusOverlay: {
|
statusOverlay: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
|
||||||
|
|
@ -405,10 +405,10 @@ const LibraryScreen = () => {
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black, borderRadius: settings.posterBorderRadius ?? 12 }]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||||
style={styles.poster}
|
style={[styles.poster, { borderRadius: settings.posterBorderRadius ?? 12 }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
{item.watched && (
|
{item.watched && (
|
||||||
|
|
@ -1063,11 +1063,14 @@ const styles = StyleSheet.create({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||||
aspectRatio: 2 / 3,
|
aspectRatio: 2 / 3,
|
||||||
elevation: 5,
|
// Consistent shadow/elevation matching ContentItem
|
||||||
shadowOffset: { width: 0, height: 4 },
|
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||||
shadowOpacity: 0.2,
|
shadowColor: '#000',
|
||||||
shadowRadius: 8,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
borderWidth: 1,
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 1,
|
||||||
|
// Consistent border styling
|
||||||
|
borderWidth: 1.5,
|
||||||
borderColor: 'rgba(255,255,255,0.15)',
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
},
|
},
|
||||||
poster: {
|
poster: {
|
||||||
|
|
|
||||||
|
|
@ -1014,16 +1014,14 @@ const SearchScreen = () => {
|
||||||
>
|
>
|
||||||
<View style={[styles.horizontalItemPosterContainer, {
|
<View style={[styles.horizontalItemPosterContainer, {
|
||||||
width: itemWidth,
|
width: itemWidth,
|
||||||
height: undefined, // Let aspect ratio control height or keep fixed height with width?
|
height: undefined, // Let aspect ratio control height
|
||||||
// Actually, since we derived width from fixed height, we can keep height fixed or use aspect.
|
|
||||||
// Using aspect ratio is safer if baseHeight changes.
|
|
||||||
aspectRatio: aspectRatio,
|
aspectRatio: aspectRatio,
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
borderColor: 'rgba(255,255,255,0.05)'
|
borderRadius: settings.posterBorderRadius ?? 12,
|
||||||
}]}>
|
}]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
||||||
style={styles.horizontalItemPoster}
|
style={[styles.horizontalItemPoster, { borderRadius: settings.posterBorderRadius ?? 12 }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
{/* Bookmark and watched icons top right, bookmark to the left of watched */}
|
{/* Bookmark and watched icons top right, bookmark to the left of watched */}
|
||||||
|
|
@ -1723,10 +1721,17 @@ const styles = StyleSheet.create({
|
||||||
horizontalItemPosterContainer: {
|
horizontalItemPosterContainer: {
|
||||||
width: HORIZONTAL_ITEM_WIDTH,
|
width: HORIZONTAL_ITEM_WIDTH,
|
||||||
height: HORIZONTAL_POSTER_HEIGHT,
|
height: HORIZONTAL_POSTER_HEIGHT,
|
||||||
borderRadius: 16,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1.5,
|
||||||
|
borderColor: 'rgba(255,255,255,0.15)',
|
||||||
|
// Consistent shadow/elevation matching ContentItem
|
||||||
|
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 1,
|
||||||
},
|
},
|
||||||
horizontalItemPoster: {
|
horizontalItemPoster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue