mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Enhance HomeScreen and StreamsScreen with landscape orientation locking and improved loading state management
This update introduces the use of the expo-screen-orientation library to lock the screen orientation to landscape mode when navigating to the Player component from both HomeScreen and StreamsScreen. Additionally, it refines loading state management in StreamsScreen by implementing guards to prevent excessive re-renders and ensuring accurate provider status updates. The changes contribute to a smoother user experience during video playback and improved performance across the application.
This commit is contained in:
parent
9465486a47
commit
45b2d4a124
3 changed files with 939 additions and 577 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -16,7 +16,8 @@ import {
|
|||
Platform,
|
||||
Image,
|
||||
Modal,
|
||||
Pressable
|
||||
Pressable,
|
||||
Alert
|
||||
} from 'react-native';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
|
|
@ -60,6 +61,7 @@ import { SkeletonFeatured } from '../components/home/SkeletonLoaders';
|
|||
import homeStyles, { sharedStyles } from '../styles/homeStyles';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import type { Theme } from '../contexts/ThemeContext';
|
||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||
|
||||
// Define interfaces for our data
|
||||
interface Category {
|
||||
|
|
@ -517,18 +519,37 @@ const HomeScreen = () => {
|
|||
navigation.navigate('Metadata', { id, type });
|
||||
}, [navigation]);
|
||||
|
||||
const handlePlayStream = useCallback((stream: Stream) => {
|
||||
const handlePlayStream = useCallback(async (stream: Stream) => {
|
||||
if (!featuredContent) return;
|
||||
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: featuredContent.name,
|
||||
year: featuredContent.year,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
streamProvider: stream.name,
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
try {
|
||||
// Lock orientation to landscape before navigation to prevent glitches
|
||||
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
||||
|
||||
// Small delay to ensure orientation is set before navigation
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: featuredContent.name,
|
||||
year: featuredContent.year,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
streamProvider: stream.name,
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback: navigate anyway
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: featuredContent.name,
|
||||
year: featuredContent.year,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
streamProvider: stream.name,
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
}
|
||||
}, [featuredContent, navigation]);
|
||||
|
||||
const refreshContinueWatching = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useMemo, memo, useState, useEffect } from 'react';
|
||||
import React, { useCallback, useMemo, memo, useState, useEffect, useRef, useLayoutEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -13,8 +13,9 @@ import {
|
|||
StatusBar,
|
||||
Alert,
|
||||
Dimensions,
|
||||
Linking
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
|
|
@ -241,10 +242,22 @@ export const StreamsScreen = () => {
|
|||
const { currentTheme } = useTheme();
|
||||
const { colors } = currentTheme;
|
||||
|
||||
// Add ref to prevent excessive updates
|
||||
const isMounted = useRef(true);
|
||||
const loadStartTimeRef = useRef(0);
|
||||
const hasDoneInitialLoadRef = useRef(false);
|
||||
|
||||
// Add timing logs
|
||||
const [loadStartTime, setLoadStartTime] = useState(0);
|
||||
const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({});
|
||||
|
||||
// Prevent excessive re-renders by using this guard
|
||||
const guardedSetState = useCallback((setter: () => void) => {
|
||||
if (isMounted.current) {
|
||||
setter();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const {
|
||||
metadata,
|
||||
episodes,
|
||||
|
|
@ -285,54 +298,64 @@ export const StreamsScreen = () => {
|
|||
}
|
||||
}>({});
|
||||
|
||||
// Monitor streams loading start and completion
|
||||
// Monitor streams loading start and completion - FIXED to prevent loops
|
||||
useEffect(() => {
|
||||
// Skip processing if component is unmounting
|
||||
if (!isMounted.current) return;
|
||||
|
||||
const now = Date.now();
|
||||
// Define all providers you expect to load. This could be dynamic.
|
||||
const expectedProviders = ['stremio', 'hdrezka'];
|
||||
|
||||
// Prevent infinite rerendering by using refs
|
||||
if (loadingStreams || loadingEpisodeStreams) {
|
||||
// --- Stream Loading has STARTED or is IN PROGRESS ---
|
||||
logger.log("⏱️ Stream loading started or in progress...");
|
||||
|
||||
// Set load start time only if this is the beginning of a new loading cycle
|
||||
if (loadStartTime === 0) {
|
||||
// Only log once when loading starts
|
||||
if (loadStartTimeRef.current === 0) {
|
||||
logger.log("⏱️ Stream loading started or in progress...");
|
||||
// Update ref directly to avoid render cycle
|
||||
loadStartTimeRef.current = now;
|
||||
// Also update state for components that need it
|
||||
setLoadStartTime(now);
|
||||
}
|
||||
|
||||
setProviderLoadTimes({}); // Reset individual provider load times tracker
|
||||
// Only update these once per loading cycle
|
||||
if (!hasDoneInitialLoadRef.current) {
|
||||
hasDoneInitialLoadRef.current = true;
|
||||
|
||||
// Use the guarded setState to prevent issues after unmount
|
||||
guardedSetState(() => setProviderLoadTimes({}));
|
||||
|
||||
// Update provider status to loading for all expected providers
|
||||
setProviderStatus(prevStatus => {
|
||||
const newStatus = { ...prevStatus };
|
||||
expectedProviders.forEach(providerId => {
|
||||
// If not already marked as loading, or if it's a fresh cycle, set to loading
|
||||
if (!newStatus[providerId] || !newStatus[providerId].loading || loadStartTime === 0) {
|
||||
newStatus[providerId] = {
|
||||
loading: true,
|
||||
success: false,
|
||||
error: false,
|
||||
message: 'Loading...',
|
||||
timeStarted: (newStatus[providerId]?.loading && newStatus[providerId]?.timeStarted) ? newStatus[providerId].timeStarted : now,
|
||||
timeCompleted: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
return newStatus;
|
||||
});
|
||||
// Update provider status to loading for all expected providers
|
||||
guardedSetState(() => setProviderStatus(prevStatus => {
|
||||
const newStatus = { ...prevStatus };
|
||||
expectedProviders.forEach(providerId => {
|
||||
// If not already marked as loading, or if it's a fresh cycle, set to loading
|
||||
if (!newStatus[providerId] || !newStatus[providerId].loading) {
|
||||
newStatus[providerId] = {
|
||||
loading: true,
|
||||
success: false,
|
||||
error: false,
|
||||
message: 'Loading...',
|
||||
timeStarted: (newStatus[providerId]?.loading && newStatus[providerId]?.timeStarted) ? newStatus[providerId].timeStarted : now,
|
||||
timeCompleted: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
return newStatus;
|
||||
}));
|
||||
|
||||
// Update simple loading flag for all expected providers
|
||||
setLoadingProviders(prevLoading => {
|
||||
const newLoading = { ...prevLoading };
|
||||
expectedProviders.forEach(providerId => {
|
||||
newLoading[providerId] = true;
|
||||
});
|
||||
return newLoading;
|
||||
});
|
||||
|
||||
} else if (loadStartTime > 0) {
|
||||
// Update simple loading flag for all expected providers
|
||||
guardedSetState(() => setLoadingProviders(prevLoading => {
|
||||
const newLoading = { ...prevLoading };
|
||||
expectedProviders.forEach(providerId => {
|
||||
newLoading[providerId] = true;
|
||||
});
|
||||
return newLoading;
|
||||
}));
|
||||
}
|
||||
} else if (loadStartTimeRef.current > 0) {
|
||||
// --- Stream Loading has FINISHED ---
|
||||
// (loadStartTime > 0 implies a loading cycle was active and has now completed)
|
||||
logger.log("🏁 Stream loading finished. Processing results.");
|
||||
|
||||
const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams;
|
||||
|
|
@ -344,56 +367,53 @@ export const StreamsScreen = () => {
|
|||
|
||||
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
|
||||
|
||||
// Update simple loading flag: all expected providers are no longer loading
|
||||
setLoadingProviders(prevLoading => {
|
||||
const newLoading = { ...prevLoading };
|
||||
expectedProviders.forEach(providerId => {
|
||||
newLoading[providerId] = false;
|
||||
});
|
||||
return newLoading;
|
||||
});
|
||||
// Reset refs for next load cycle
|
||||
loadStartTimeRef.current = 0;
|
||||
hasDoneInitialLoadRef.current = false;
|
||||
|
||||
// Update states only if component is still mounted
|
||||
if (isMounted.current) {
|
||||
// Update simple loading flag: all expected providers are no longer loading
|
||||
guardedSetState(() => setLoadingProviders(prevLoading => {
|
||||
const newLoading = { ...prevLoading };
|
||||
expectedProviders.forEach(providerId => {
|
||||
newLoading[providerId] = false;
|
||||
});
|
||||
return newLoading;
|
||||
}));
|
||||
|
||||
// Update detailed provider status based on results
|
||||
setProviderStatus(prevStatus => {
|
||||
const newStatus = { ...prevStatus };
|
||||
expectedProviders.forEach(providerId => {
|
||||
if (newStatus[providerId]) { // Ensure the provider entry exists
|
||||
const providerHasStreams = currentStreamsData[providerId] &&
|
||||
currentStreamsData[providerId].streams &&
|
||||
currentStreamsData[providerId].streams.length > 0;
|
||||
|
||||
newStatus[providerId] = {
|
||||
...newStatus[providerId], // Preserve timeStarted
|
||||
loading: false,
|
||||
success: providerHasStreams,
|
||||
// Mark error if it was loading and now no streams, and wasn't already successful
|
||||
error: !providerHasStreams && newStatus[providerId].loading && !newStatus[providerId].success,
|
||||
message: providerHasStreams ? 'Loaded successfully' : (newStatus[providerId].error ? 'Error or no streams' : 'No streams found'),
|
||||
timeCompleted: now,
|
||||
};
|
||||
} else {
|
||||
// Fallback if somehow not initialized (should be caught by loading phase)
|
||||
newStatus[providerId] = {
|
||||
loading: false,
|
||||
success: false,
|
||||
error: true,
|
||||
message: 'Provider status error (not initialized)',
|
||||
timeStarted: 0,
|
||||
timeCompleted: now,
|
||||
};
|
||||
}
|
||||
});
|
||||
return newStatus;
|
||||
});
|
||||
// Update detailed provider status based on results
|
||||
guardedSetState(() => setProviderStatus(prevStatus => {
|
||||
const newStatus = { ...prevStatus };
|
||||
expectedProviders.forEach(providerId => {
|
||||
if (newStatus[providerId]) { // Ensure the provider entry exists
|
||||
const providerHasStreams = currentStreamsData[providerId] &&
|
||||
currentStreamsData[providerId].streams &&
|
||||
currentStreamsData[providerId].streams.length > 0;
|
||||
|
||||
newStatus[providerId] = {
|
||||
...newStatus[providerId], // Preserve timeStarted
|
||||
loading: false,
|
||||
success: providerHasStreams,
|
||||
// Mark error if it was loading and now no streams, and wasn't already successful
|
||||
error: !providerHasStreams && newStatus[providerId].loading && !newStatus[providerId].success,
|
||||
message: providerHasStreams ? 'Loaded successfully' : (newStatus[providerId].error ? 'Error or no streams' : 'No streams found'),
|
||||
timeCompleted: now,
|
||||
};
|
||||
}
|
||||
});
|
||||
return newStatus;
|
||||
}));
|
||||
|
||||
// Update the set of available providers based on what actually loaded streams
|
||||
const providersWithStreamsSet = new Set(providersWithStreams);
|
||||
setAvailableProviders(providersWithStreamsSet);
|
||||
// Update the set of available providers based on what actually loaded streams
|
||||
const providersWithStreamsSet = new Set(providersWithStreams);
|
||||
guardedSetState(() => setAvailableProviders(providersWithStreamsSet));
|
||||
|
||||
// Reset loadStartTime to signify the end of this loading cycle
|
||||
setLoadStartTime(0);
|
||||
// Reset loadStartTime to signify the end of this loading cycle
|
||||
guardedSetState(() => setLoadStartTime(0));
|
||||
}
|
||||
}
|
||||
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type /* loadStartTime is intentionally omitted from deps here */]);
|
||||
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type, guardedSetState]);
|
||||
|
||||
// Add useEffect to update availableProviders whenever streams change
|
||||
useEffect(() => {
|
||||
|
|
@ -487,20 +507,44 @@ export const StreamsScreen = () => {
|
|||
);
|
||||
}, [selectedEpisode, groupedEpisodes, id]);
|
||||
|
||||
const navigateToPlayer = useCallback((stream: Stream) => {
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
year: metadata?.year,
|
||||
streamProvider: stream.name,
|
||||
id,
|
||||
type,
|
||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
|
||||
});
|
||||
const navigateToPlayer = useCallback(async (stream: Stream) => {
|
||||
try {
|
||||
// Lock orientation to landscape before navigation to prevent glitches
|
||||
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
||||
|
||||
// Small delay to ensure orientation is set before navigation
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
year: metadata?.year,
|
||||
streamProvider: stream.name,
|
||||
id,
|
||||
type,
|
||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[StreamsScreen] Error locking orientation before navigation:', error);
|
||||
// Fallback: navigate anyway
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
title: metadata?.name || '',
|
||||
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
|
||||
season: type === 'series' ? currentEpisode?.season_number : undefined,
|
||||
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
|
||||
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
|
||||
year: metadata?.year,
|
||||
streamProvider: stream.name,
|
||||
id,
|
||||
type,
|
||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
|
||||
});
|
||||
}
|
||||
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode]);
|
||||
|
||||
// Update handleStreamPress
|
||||
|
|
@ -834,6 +878,13 @@ export const StreamsScreen = () => {
|
|||
</Animated.View>
|
||||
), [styles.streamGroupTitle]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue