diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 586a294a..1da36a1b 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -711,55 +711,112 @@ const AndroidVideoPlayer: React.FC = () => { }; const handleError = (error: any) => { - logger.error('AndroidVideoPlayer error: ', error); - - // Check for specific AVFoundation server configuration errors - const isServerConfigError = error?.error?.code === -11850 || - error?.code === -11850 || - (error?.error?.localizedDescription && - error.error.localizedDescription.includes('server is not correctly configured')); - - // Format error details for user display - let errorMessage = 'An unknown error occurred'; - if (error) { - if (isServerConfigError) { - errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.'; - } else if (typeof error === 'string') { - errorMessage = error; - } else if (error.message) { - errorMessage = error.message; - } else if (error.error && error.error.message) { - errorMessage = error.error.message; - } else if (error.error && error.error.localizedDescription) { - errorMessage = error.error.localizedDescription; - } else if (error.code) { - errorMessage = `Error Code: ${error.code}`; - } else { - errorMessage = JSON.stringify(error, null, 2); + try { + logger.error('AndroidVideoPlayer error: ', error); + + // Early return if component is unmounted to prevent iOS crashes + if (!isMounted.current) { + logger.warn('[AndroidVideoPlayer] Component unmounted, skipping error handling'); + return; + } + + // Check for specific AVFoundation server configuration errors + const isServerConfigError = error?.error?.code === -11850 || + error?.code === -11850 || + (error?.error?.localizedDescription && + error.error.localizedDescription.includes('server is not correctly configured')); + + // Format error details for user display + let errorMessage = 'An unknown error occurred'; + if (error) { + if (isServerConfigError) { + errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.'; + } else if (typeof error === 'string') { + errorMessage = error; + } else if (error.message) { + errorMessage = error.message; + } else if (error.error && error.error.message) { + errorMessage = error.error.message; + } else if (error.error && error.error.localizedDescription) { + errorMessage = error.error.localizedDescription; + } else if (error.code) { + errorMessage = `Error Code: ${error.code}`; + } else { + try { + errorMessage = JSON.stringify(error, null, 2); + } catch (jsonError) { + errorMessage = 'Error occurred but details could not be serialized'; + } + } + } + + // Use safeSetState to prevent crashes on iOS when component is unmounted + safeSetState(() => { + setErrorDetails(errorMessage); + setShowErrorModal(true); + }); + + // Clear any existing timeout + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + + // Auto-exit after 5 seconds if user doesn't dismiss + errorTimeoutRef.current = setTimeout(() => { + if (isMounted.current) { + handleErrorExit(); + } + }, 5000); + } catch (handlerError) { + // Fallback error handling to prevent crashes during error processing + logger.error('[AndroidVideoPlayer] Error in error handler:', handlerError); + if (isMounted.current) { + // Minimal safe error handling + safeSetState(() => { + setErrorDetails('A critical error occurred'); + setShowErrorModal(true); + }); + // Force exit after 3 seconds if error handler itself fails + setTimeout(() => { + if (isMounted.current) { + handleClose(); + } + }, 3000); } } - - setErrorDetails(errorMessage); - setShowErrorModal(true); - - // Clear any existing timeout - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - } - - // Auto-exit after 5 seconds if user doesn't dismiss - errorTimeoutRef.current = setTimeout(() => { - handleErrorExit(); - }, 5000); }; const handleErrorExit = () => { - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - errorTimeoutRef.current = null; + try { + // Early return if component is unmounted + if (!isMounted.current) { + logger.warn('[AndroidVideoPlayer] Component unmounted, skipping error exit'); + return; + } + + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + errorTimeoutRef.current = null; + } + + // Use safeSetState to prevent crashes on iOS when component is unmounted + safeSetState(() => { + setShowErrorModal(false); + }); + + // Add small delay before closing to ensure modal state is updated + setTimeout(() => { + if (isMounted.current) { + handleClose(); + } + }, 100); + } catch (exitError) { + logger.error('[AndroidVideoPlayer] Error in handleErrorExit:', exitError); + // Force close as last resort + if (isMounted.current) { + handleClose(); + } } - setShowErrorModal(false); - handleClose(); }; const onBuffer = (data: any) => { @@ -1323,12 +1380,15 @@ const AndroidVideoPlayer: React.FC = () => { /> {/* Error Modal */} - + {isMounted.current && ( + { }}>This dialog will auto-close in 5 seconds - + + )} ); }; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index e1a99a58..ec13038f 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -43,6 +43,7 @@ export interface AppSettings { enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name streamSortMode: 'scraper-then-quality' | 'quality-then-scraper'; // How to sort streams - by scraper first or quality first + showScraperLogos: boolean; // Show/hide scraper logos next to streaming links // Quality filtering settings excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p']) } @@ -70,6 +71,7 @@ export const DEFAULT_SETTINGS: AppSettings = { enableScraperUrlValidation: true, // Enable URL validation by default streamDisplayMode: 'separate', // Default to separate display by provider streamSortMode: 'scraper-then-quality', // Default to current behavior (scraper first, then quality) + showScraperLogos: true, // Show scraper logos by default // Quality filtering defaults excludedQualities: [], // No qualities excluded by default }; diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 75624907..460c86f1 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -824,6 +824,22 @@ const PluginsScreen: React.FC = () => { disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'} /> + + + + Show Scraper Logos + + Display scraper logos next to streaming links on the streams screen. + + + updateSetting('showScraperLogos', value)} + trackColor={{ false: colors.elevation3, true: colors.primary }} + thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} + disabled={!settings.enableLocalScrapers} + /> + {/* Quality Filtering */} diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 07ec6cf1..19c620b2 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -62,13 +62,14 @@ const scraperLogoCache = new Map(); let scraperLogoCachePromise: Promise | null = null; // Extracted Components -const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme }: { +const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos }: { stream: Stream; onPress: () => void; index: number; isLoading?: boolean; statusMessage?: string; theme: any; + showLogos?: boolean; }) => { const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); @@ -157,7 +158,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the activeOpacity={0.7} > {/* Scraper Logo */} - {scraperLogo && ( + {showLogos && scraperLogo && ( { isLoading={isLoading} statusMessage={undefined} theme={currentTheme} + showLogos={settings.showScraperLogos} /> ); - }, [handleStreamPress, currentTheme]); + }, [handleStreamPress, currentTheme, settings.showScraperLogos]); const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => { const isProviderLoading = loadingProviders[section.addonId];