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];