Ios #14
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