mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
focus enabled for hoemscreena and metascreen
This commit is contained in:
parent
703c3e3cfb
commit
18c18257c9
12 changed files with 693 additions and 640 deletions
11
App.tsx
11
App.tsx
|
|
@ -120,7 +120,14 @@ const ThemedApp = () => {
|
|||
try {
|
||||
// Check onboarding status
|
||||
const onboardingCompleted = await mmkvStorage.getItem('hasCompletedOnboarding');
|
||||
setHasCompletedOnboarding(onboardingCompleted === 'true');
|
||||
|
||||
// On TV, auto-complete onboarding to skip the tutorial screens
|
||||
if (Platform.isTV && onboardingCompleted !== 'true') {
|
||||
await mmkvStorage.setItem('hasCompletedOnboarding', 'true');
|
||||
setHasCompletedOnboarding(true);
|
||||
} else {
|
||||
setHasCompletedOnboarding(onboardingCompleted === 'true');
|
||||
}
|
||||
|
||||
// Initialize update service
|
||||
await UpdateService.initialize();
|
||||
|
|
@ -135,7 +142,7 @@ const ThemedApp = () => {
|
|||
|
||||
// Check if announcement should be shown (version 1.0.0)
|
||||
const announcementShown = await mmkvStorage.getItem('announcement_v1.0.0_shown');
|
||||
if (!announcementShown && onboardingCompleted === 'true') {
|
||||
if (!announcementShown && (onboardingCompleted === 'true' || Platform.isTV) && !Platform.isTV) {
|
||||
// Show announcement only after app is ready
|
||||
setTimeout(() => {
|
||||
setShowAnnouncement(true);
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
});
|
||||
}
|
||||
}}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
|
|
@ -1197,7 +1198,6 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
]}
|
||||
onLayout={(event) => {
|
||||
const { height } = event.nativeEvent.layout;
|
||||
setLogoHeights((prev) => ({ ...prev, [currentIndex]: height }));
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
|
|
@ -1223,6 +1223,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
});
|
||||
}
|
||||
}}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title} numberOfLines={2}>
|
||||
|
|
@ -1256,6 +1257,8 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
style={[styles.playButton]}
|
||||
onPress={handlePlayAction}
|
||||
activeOpacity={0.85}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={playButtonText === 'Resume' ? "replay" : "play-arrow"}
|
||||
|
|
@ -1270,6 +1273,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
style={styles.saveButton}
|
||||
onPress={handleSaveAction}
|
||||
activeOpacity={0.85}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={inLibrary ? "bookmark" : "bookmark-outline"}
|
||||
|
|
|
|||
|
|
@ -443,7 +443,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
<LinearGradient
|
||||
colors={[
|
||||
'transparent',
|
||||
'transparent',
|
||||
'transparent',
|
||||
'rgba(0,0,0,0.3)',
|
||||
'rgba(0,0,0,0.7)',
|
||||
'rgba(0,0,0,0.95)'
|
||||
|
|
@ -459,7 +459,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
{logoUrl && !logoLoadError ? (
|
||||
<Animated.View style={logoAnimatedStyle}>
|
||||
<FastImage
|
||||
source={{
|
||||
source={{
|
||||
uri: logoUrl,
|
||||
priority: FastImage.priority.high,
|
||||
cache: FastImage.cacheControl.immutable
|
||||
|
|
@ -506,6 +506,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
}
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
|
|
@ -536,7 +537,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* Bottom fade to blend with background */}
|
||||
<LinearGradient
|
||||
colors={[
|
||||
|
|
@ -589,7 +590,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
{logoUrl && !logoLoadError ? (
|
||||
<Animated.View style={logoAnimatedStyle}>
|
||||
<FastImage
|
||||
source={{
|
||||
source={{
|
||||
uri: logoUrl,
|
||||
priority: FastImage.priority.high,
|
||||
cache: FastImage.cacheControl.immutable
|
||||
|
|
@ -641,6 +642,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
}
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
|
|
@ -663,7 +665,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
</ImageBackground>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
{/* Bottom fade to blend with background */}
|
||||
<LinearGradient
|
||||
colors={[
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
|||
{(loopingEnabled ? loopData : data).map((item, index) => (
|
||||
/* TEST 5: ORIGINAL CARD WITHOUT LINEAR GRADIENT */
|
||||
<CarouselCard
|
||||
hasTVPreferredFocus={Platform.isTV && (loopingEnabled ? index === 1 : index === 0)}
|
||||
key={`${item.id}-${index}-${loopingEnabled ? 'loop' : 'base'}`}
|
||||
item={item}
|
||||
colors={currentTheme.colors}
|
||||
|
|
@ -607,9 +608,10 @@ interface CarouselCardProps {
|
|||
cardWidth: number;
|
||||
cardHeight: number;
|
||||
isTablet: boolean;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
}
|
||||
|
||||
const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index, flipped, onToggleFlip, interval, cardWidth, cardHeight, isTablet }) => {
|
||||
const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index, flipped, onToggleFlip, interval, cardWidth, cardHeight, isTablet, hasTVPreferredFocus }) => {
|
||||
const [bannerLoaded, setBannerLoaded] = useState(false);
|
||||
const [logoLoaded, setLogoLoaded] = useState(false);
|
||||
|
||||
|
|
@ -851,13 +853,13 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
|||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={onPressInfo} style={StyleSheet.absoluteFillObject as any} />
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={onPressInfo} style={StyleSheet.absoluteFillObject as any} hasTVPreferredFocus={hasTVPreferredFocus} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* FRONT FACE */}
|
||||
<Animated.View style={[styles.flipFace as any, styles.frontFace as any, frontFlipStyle]} pointerEvents={flipped ? 'none' : 'auto'}>
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={onPressInfo} style={StyleSheet.absoluteFillObject as any}>
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={onPressInfo} style={StyleSheet.absoluteFillObject as any} hasTVPreferredFocus={hasTVPreferredFocus && !isTablet}>
|
||||
<View style={styles.bannerContainer as ViewStyle}>
|
||||
{!bannerLoaded && (
|
||||
<View style={styles.skeletonBannerFull as ViewStyle} />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import Animated, {
|
||||
|
|
@ -40,7 +41,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
// 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';
|
||||
|
|
@ -48,13 +49,13 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
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) {
|
||||
|
|
@ -68,7 +69,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
return 16; // phone
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
// Enhanced cast card sizing
|
||||
const castCardWidth = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
|
|
@ -82,7 +83,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
return 90; // phone
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
const castImageSize = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
case 'tv':
|
||||
|
|
@ -95,7 +96,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
return 80; // phone
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
const castCardSpacing = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
case 'tv':
|
||||
|
|
@ -122,7 +123,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
<Animated.View
|
||||
style={styles.castSection}
|
||||
entering={FadeIn.duration(300).delay(150)}
|
||||
>
|
||||
|
|
@ -131,8 +132,8 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
{ paddingHorizontal: horizontalPadding }
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.sectionTitle,
|
||||
{
|
||||
styles.sectionTitle,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18,
|
||||
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
|
||||
|
|
@ -149,10 +150,10 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
]}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item, index }) => (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300).delay(50 + index * 30)}
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300).delay(50 + index * 30)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.castCard,
|
||||
{
|
||||
|
|
@ -182,15 +183,15 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
/>
|
||||
) : (
|
||||
<View style={[
|
||||
styles.castImagePlaceholder,
|
||||
{
|
||||
styles.castImagePlaceholder,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
borderRadius: castImageSize / 2
|
||||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.placeholderText,
|
||||
{
|
||||
styles.placeholderText,
|
||||
{
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
|
||||
}
|
||||
|
|
@ -201,8 +202,8 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
)}
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.castName,
|
||||
{
|
||||
styles.castName,
|
||||
{
|
||||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
|
||||
width: castCardWidth
|
||||
|
|
@ -210,8 +211,8 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
]} numberOfLines={1}>{item.name}</Text>
|
||||
{isTmdbEnrichmentEnabled && item.character && (
|
||||
<Text style={[
|
||||
styles.characterName,
|
||||
{
|
||||
styles.characterName,
|
||||
{
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12,
|
||||
width: castCardWidth,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
|
|
@ -34,10 +35,10 @@ interface CollectionSectionProps {
|
|||
loadingCollection: boolean;
|
||||
}
|
||||
|
||||
export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
||||
collectionName,
|
||||
collectionMovies,
|
||||
loadingCollection
|
||||
export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
||||
collectionName,
|
||||
collectionMovies,
|
||||
loadingCollection
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -82,7 +83,7 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
default: return 180;
|
||||
}
|
||||
}, [deviceType]);
|
||||
const backdropHeight = React.useMemo(() => backdropWidth * (9/16), [backdropWidth]); // 16:9 aspect ratio
|
||||
const backdropHeight = React.useMemo(() => backdropWidth * (9 / 16), [backdropWidth]); // 16:9 aspect ratio
|
||||
|
||||
const [alertVisible, setAlertVisible] = React.useState(false);
|
||||
const [alertTitle, setAlertTitle] = React.useState('');
|
||||
|
|
@ -93,15 +94,15 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
try {
|
||||
// Extract TMDB ID from the tmdb:123456 format
|
||||
const tmdbId = item.id.replace('tmdb:', '');
|
||||
|
||||
|
||||
// Get Stremio ID directly using catalogService
|
||||
const stremioId = await catalogService.getStremioId(item.type, tmdbId);
|
||||
|
||||
|
||||
if (stremioId) {
|
||||
navigation.dispatch(
|
||||
StackActions.push('Metadata', {
|
||||
id: stremioId,
|
||||
type: item.type
|
||||
StackActions.push('Metadata', {
|
||||
id: stremioId,
|
||||
type: item.type
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
|
@ -111,7 +112,7 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
if (__DEV__) console.error('Error navigating to collection item:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Unable to load this content. Please try again later.');
|
||||
setAlertActions([{ label: 'OK', onPress: () => {} }]);
|
||||
setAlertActions([{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
};
|
||||
|
|
@ -120,9 +121,9 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
// Upcoming/unreleased movies without a year will be sorted last
|
||||
const sortedCollectionMovies = React.useMemo(() => {
|
||||
if (!collectionMovies) return [];
|
||||
|
||||
|
||||
const FUTURE_YEAR_PLACEHOLDER = 9999; // Very large number to sort unreleased movies last
|
||||
|
||||
|
||||
return [...collectionMovies].sort((a, b) => {
|
||||
// Treat missing years as future year placeholder (sorts last)
|
||||
const yearA = a.year ? parseInt(a.year.toString()) : FUTURE_YEAR_PLACEHOLDER;
|
||||
|
|
@ -132,31 +133,31 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
}, [collectionMovies]);
|
||||
|
||||
const renderItem = ({ item }: { item: StreamingContent }) => (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width: backdropWidth, marginRight: itemSpacing }]}
|
||||
onPress={() => handleItemPress(item)}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: item.banner || item.poster }}
|
||||
style={[styles.backdrop, {
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
width: backdropWidth,
|
||||
height: backdropHeight,
|
||||
borderRadius: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 10 : 8
|
||||
style={[styles.backdrop, {
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
width: backdropWidth,
|
||||
height: backdropHeight,
|
||||
borderRadius: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 10 : 8
|
||||
}]}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<Text style={[styles.title, {
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13,
|
||||
lineHeight: isTV ? 20 : 18
|
||||
<Text style={[styles.title, {
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13,
|
||||
lineHeight: isTV ? 20 : 18
|
||||
}]} numberOfLines={2}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.year && (
|
||||
<Text style={[styles.year, {
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 11 : 11
|
||||
<Text style={[styles.year, {
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 11 : 11
|
||||
}]}>
|
||||
{item.year}
|
||||
</Text>
|
||||
|
|
@ -177,11 +178,11 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingLeft: 0 }] }>
|
||||
<Text style={[styles.sectionTitle, {
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
|
||||
paddingHorizontal: horizontalPadding
|
||||
<View style={[styles.container, { paddingLeft: 0 }]}>
|
||||
<Text style={[styles.sectionTitle, {
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
|
||||
paddingHorizontal: horizontalPadding
|
||||
}]}>
|
||||
{collectionName}
|
||||
</Text>
|
||||
|
|
@ -191,9 +192,9 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
keyExtractor={(item) => item.id}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.listContentContainer, {
|
||||
paddingHorizontal: horizontalPadding,
|
||||
paddingRight: horizontalPadding + itemSpacing
|
||||
contentContainerStyle={[styles.listContentContainer, {
|
||||
paddingHorizontal: horizontalPadding,
|
||||
paddingRight: horizontalPadding + itemSpacing
|
||||
}]}
|
||||
/>
|
||||
<CustomAlert
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@ import {
|
|||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
|
|
@ -33,9 +34,9 @@ interface MoreLikeThisSectionProps {
|
|||
loadingRecommendations: boolean;
|
||||
}
|
||||
|
||||
export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||
recommendations,
|
||||
loadingRecommendations
|
||||
export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||
recommendations,
|
||||
loadingRecommendations
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -91,16 +92,16 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
try {
|
||||
// Extract TMDB ID from the tmdb:123456 format
|
||||
const tmdbId = item.id.replace('tmdb:', '');
|
||||
|
||||
|
||||
// Get Stremio ID directly using catalogService
|
||||
// The catalogService.getStremioId method already handles the conversion internally
|
||||
const stremioId = await catalogService.getStremioId(item.type, tmdbId);
|
||||
|
||||
|
||||
if (stremioId) {
|
||||
navigation.dispatch(
|
||||
StackActions.push('Metadata', {
|
||||
id: stremioId,
|
||||
type: item.type
|
||||
StackActions.push('Metadata', {
|
||||
id: stremioId,
|
||||
type: item.type
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
|
@ -110,13 +111,13 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
if (__DEV__) console.error('Error navigating to recommendation:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Unable to load this content. Please try again later.');
|
||||
setAlertActions([{ label: 'OK', onPress: () => {} }]);
|
||||
setAlertActions([{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: StreamingContent }) => (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
|
||||
onPress={() => handleItemPress(item)}
|
||||
>
|
||||
|
|
@ -144,7 +145,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
}
|
||||
|
||||
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>
|
||||
<FlatList
|
||||
data={recommendations}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState, useRef, useCallback, useMemo, memo } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme, FlatList, Modal, Pressable } from 'react-native';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme, FlatList, Modal, Pressable, Platform } from 'react-native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -794,12 +794,8 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
borderRadius: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6
|
||||
}
|
||||
]}
|
||||
onPress={() => {
|
||||
const newMode = seasonViewMode === 'posters' ? 'text' : 'posters';
|
||||
updateViewMode(newMode);
|
||||
if (__DEV__) console.log('[SeriesContent] View mode changed to:', newMode, 'Current ref value:', seasonViewMode);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<Text style={[
|
||||
styles.seasonViewToggleText,
|
||||
|
|
@ -863,6 +859,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
selectedSeason === season && styles.selectedSeasonTextButton
|
||||
]}
|
||||
onPress={() => onSeasonChange(season)}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<Text style={[
|
||||
styles.seasonTextButtonText,
|
||||
|
|
@ -898,6 +895,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
selectedSeason === season && [styles.selectedSeasonButton, { borderColor: currentTheme.colors.primary }]
|
||||
]}
|
||||
onPress={() => onSeasonChange(season)}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<View style={[
|
||||
styles.seasonPosterContainer,
|
||||
|
|
@ -945,7 +943,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
}}
|
||||
keyExtractor={season => season.toString()}
|
||||
/>
|
||||
</View>
|
||||
</View >
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -1039,6 +1037,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
onLongPress={() => handleEpisodeLongPress(episode)}
|
||||
delayLongPress={400}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<View style={[
|
||||
styles.episodeImageContainer,
|
||||
|
|
@ -1322,6 +1321,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
onLongPress={() => handleEpisodeLongPress(episode)}
|
||||
delayLongPress={400}
|
||||
activeOpacity={0.85}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
{/* Solid outline replaces gradient border */}
|
||||
|
||||
|
|
|
|||
|
|
@ -530,6 +530,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
]}
|
||||
onPress={toggleDropdown}
|
||||
activeOpacity={0.8}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
|
|
@ -585,6 +586,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
]}
|
||||
onPress={() => handleCategorySelect(category)}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<View style={styles.dropdownItemContent}>
|
||||
<View style={[
|
||||
|
|
@ -664,6 +666,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
]}
|
||||
onPress={() => handleTrailerPress(trailer)}
|
||||
activeOpacity={0.9}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
{/* Thumbnail with Gradient Overlay */}
|
||||
<View style={styles.thumbnailWrapper}>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import {
|
|||
Easing,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import * as Updates from 'expo-updates';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { backupService } from '../services/backupService';
|
||||
|
|
@ -24,24 +22,40 @@ import { logger } from '../utils/logger';
|
|||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useBackupOptions } from '../hooks/useBackupOptions';
|
||||
|
||||
// Check if running on TV platform
|
||||
const isTV = Platform.isTV;
|
||||
|
||||
// Conditionally import expo-document-picker and expo-sharing (not available on TV)
|
||||
let DocumentPicker: typeof import('expo-document-picker') | null = null;
|
||||
let Sharing: typeof import('expo-sharing') | null = null;
|
||||
|
||||
if (!isTV) {
|
||||
try {
|
||||
DocumentPicker = require('expo-document-picker');
|
||||
Sharing = require('expo-sharing');
|
||||
} catch (e) {
|
||||
logger.warn('[BackupScreen] Document picker/sharing not available');
|
||||
}
|
||||
}
|
||||
|
||||
const BackupScreen: React.FC = () => {
|
||||
const { currentTheme } = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigation = useNavigation();
|
||||
const { preferences, updatePreference, getBackupOptions } = useBackupOptions();
|
||||
|
||||
|
||||
// Collapsible sections state
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
coreData: false,
|
||||
addonsIntegrations: false,
|
||||
settingsPreferences: false,
|
||||
});
|
||||
|
||||
|
||||
// Animated values for each section
|
||||
const coreDataAnim = useRef(new Animated.Value(0)).current;
|
||||
const addonsAnim = useRef(new Animated.Value(0)).current;
|
||||
const settingsAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
|
||||
// Chevron rotation animated values
|
||||
const coreDataChevron = useRef(new Animated.Value(0)).current;
|
||||
const addonsChevron = useRef(new Animated.Value(0)).current;
|
||||
|
|
@ -60,7 +74,7 @@ const BackupScreen: React.FC = () => {
|
|||
) => {
|
||||
setAlertTitle(title);
|
||||
setAlertMessage(message);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
|
|
@ -73,7 +87,7 @@ const BackupScreen: React.FC = () => {
|
|||
openAlert(
|
||||
'Restart Failed',
|
||||
'Failed to restart the app. Please manually close and reopen the app to see your restored data.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
[{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -81,10 +95,10 @@ const BackupScreen: React.FC = () => {
|
|||
// Toggle section collapse/expand
|
||||
const toggleSection = useCallback((section: 'coreData' | 'addonsIntegrations' | 'settingsPreferences') => {
|
||||
const isExpanded = expandedSections[section];
|
||||
|
||||
|
||||
let heightAnim: Animated.Value;
|
||||
let chevronAnim: Animated.Value;
|
||||
|
||||
|
||||
if (section === 'coreData') {
|
||||
heightAnim = coreDataAnim;
|
||||
chevronAnim = coreDataChevron;
|
||||
|
|
@ -95,7 +109,7 @@ const BackupScreen: React.FC = () => {
|
|||
heightAnim = settingsAnim;
|
||||
chevronAnim = settingsChevron;
|
||||
}
|
||||
|
||||
|
||||
// Animate height and chevron rotation
|
||||
Animated.parallel([
|
||||
Animated.timing(heightAnim, {
|
||||
|
|
@ -111,8 +125,8 @@ const BackupScreen: React.FC = () => {
|
|||
easing: Easing.inOut(Easing.ease),
|
||||
}),
|
||||
]).start();
|
||||
|
||||
setExpandedSections(prev => ({...prev, [section]: !isExpanded}));
|
||||
|
||||
setExpandedSections(prev => ({ ...prev, [section]: !isExpanded }));
|
||||
}, [expandedSections, coreDataAnim, addonsAnim, settingsAnim, coreDataChevron, addonsChevron, settingsChevron]);
|
||||
|
||||
// Create backup
|
||||
|
|
@ -157,51 +171,54 @@ const BackupScreen: React.FC = () => {
|
|||
message,
|
||||
items.length > 0
|
||||
? [
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{
|
||||
label: 'Create Backup',
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Create Backup',
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const backupOptions = getBackupOptions();
|
||||
const backupOptions = getBackupOptions();
|
||||
|
||||
const fileUri = await backupService.createBackup(backupOptions);
|
||||
const fileUri = await backupService.createBackup(backupOptions);
|
||||
|
||||
// Share the backup file
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(fileUri, {
|
||||
mimeType: 'application/json',
|
||||
dialogTitle: 'Share Nuvio Backup',
|
||||
});
|
||||
}
|
||||
|
||||
openAlert(
|
||||
'Backup Created',
|
||||
'Your backup has been created and is ready to share.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[BackupScreen] Failed to create backup:', error);
|
||||
openAlert(
|
||||
'Backup Failed',
|
||||
`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// Share the backup file
|
||||
if (Sharing && await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(fileUri, {
|
||||
mimeType: 'application/json',
|
||||
dialogTitle: 'Share Nuvio Backup',
|
||||
});
|
||||
} else {
|
||||
openAlert('Info', 'Backup created successfully at ' + fileUri);
|
||||
return;
|
||||
}
|
||||
|
||||
openAlert(
|
||||
'Backup Created',
|
||||
'Your backup has been created and is ready to share.',
|
||||
[{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[BackupScreen] Failed to create backup:', error);
|
||||
openAlert(
|
||||
'Backup Failed',
|
||||
`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||
[{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
]
|
||||
: [{ label: 'OK', onPress: () => {} }]
|
||||
}
|
||||
]
|
||||
: [{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[BackupScreen] Failed to get backup preview:', error);
|
||||
openAlert(
|
||||
'Error',
|
||||
'Failed to prepare backup information. Please try again.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
[{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -210,6 +227,11 @@ const BackupScreen: React.FC = () => {
|
|||
// Restore backup
|
||||
const handleRestoreBackup = useCallback(async () => {
|
||||
try {
|
||||
if (!DocumentPicker) {
|
||||
openAlert('Not Supported', 'Backup restore is not supported on this device/platform.');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
type: 'application/json',
|
||||
copyToCacheDirectory: true,
|
||||
|
|
@ -228,7 +250,7 @@ const BackupScreen: React.FC = () => {
|
|||
'Confirm Restore',
|
||||
`This will restore your data from a backup created on ${new Date(backupInfo.timestamp || 0).toLocaleDateString()}.\n\nThis action will overwrite your current data. Are you sure you want to continue?`,
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Restore',
|
||||
onPress: async () => {
|
||||
|
|
@ -243,9 +265,9 @@ const BackupScreen: React.FC = () => {
|
|||
'Restore Complete',
|
||||
'Your data has been successfully restored. Please restart the app to see all changes.',
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{
|
||||
label: 'Restart App',
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Restart App',
|
||||
onPress: restartApp,
|
||||
style: { fontWeight: 'bold' }
|
||||
}
|
||||
|
|
@ -256,7 +278,7 @@ const BackupScreen: React.FC = () => {
|
|||
openAlert(
|
||||
'Restore Failed',
|
||||
`Failed to restore backup: ${error instanceof Error ? error.message : String(error)}`,
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
[{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
@ -270,7 +292,7 @@ const BackupScreen: React.FC = () => {
|
|||
openAlert(
|
||||
'File Selection Failed',
|
||||
`Failed to select backup file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
[{ label: 'OK', onPress: () => { } }]
|
||||
);
|
||||
}
|
||||
}, [openAlert]);
|
||||
|
|
@ -281,26 +303,26 @@ const BackupScreen: React.FC = () => {
|
|||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but keeping structure consistent */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>
|
||||
Backup & Restore
|
||||
</Text>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
|
|
@ -321,7 +343,7 @@ const BackupScreen: React.FC = () => {
|
|||
<Text style={[styles.sectionDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Choose what to include in your backups
|
||||
</Text>
|
||||
|
||||
|
||||
{/* Core Data Group */}
|
||||
<TouchableOpacity
|
||||
style={styles.sectionHeader}
|
||||
|
|
@ -369,7 +391,7 @@ const BackupScreen: React.FC = () => {
|
|||
theme={currentTheme}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* Addons & Integrations Group */}
|
||||
<TouchableOpacity
|
||||
style={styles.sectionHeader}
|
||||
|
|
@ -424,7 +446,7 @@ const BackupScreen: React.FC = () => {
|
|||
theme={currentTheme}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
{/* Settings & Preferences Group */}
|
||||
<TouchableOpacity
|
||||
style={styles.sectionHeader}
|
||||
|
|
|
|||
|
|
@ -868,6 +868,7 @@ const MetadataScreen: React.FC = () => {
|
|||
<TouchableOpacity
|
||||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={loadMetadata}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
|
||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||
|
|
@ -875,6 +876,7 @@ const MetadataScreen: React.FC = () => {
|
|||
<TouchableOpacity
|
||||
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
|
||||
onPress={handleBack}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
|
||||
</TouchableOpacity>
|
||||
|
|
@ -1245,6 +1247,7 @@ const MetadataScreen: React.FC = () => {
|
|||
type: 'movie',
|
||||
title: metadata.name || 'Gallery'
|
||||
})}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
|
|
@ -1385,6 +1388,7 @@ const MetadataScreen: React.FC = () => {
|
|||
type: 'tv',
|
||||
title: metadata.name || 'Gallery'
|
||||
})}
|
||||
focusable={Platform.isTV}
|
||||
>
|
||||
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue