Merge branch 'recovered-ui-changes'

This commit is contained in:
tapframe 2025-07-30 14:02:02 +05:30
commit 9619f6b0b0
4 changed files with 134 additions and 53 deletions

View file

@ -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 */}
<Modal
visible={showErrorModal}
transparent
animationType="fade"
onRequestClose={handleErrorExit}
>
{isMounted.current && (
<Modal
visible={showErrorModal}
transparent
animationType="fade"
onRequestClose={handleErrorExit}
supportedOrientations={['landscape', 'portrait']}
statusBarTranslucent={true}
>
<View style={{
flex: 1,
justifyContent: 'center',
@ -1414,7 +1474,8 @@ const AndroidVideoPlayer: React.FC = () => {
}}>This dialog will auto-close in 5 seconds</Text>
</View>
</View>
</Modal>
</Modal>
)}
</View>
);
};

View file

@ -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
};

View file

@ -824,6 +824,22 @@ const PluginsScreen: React.FC = () => {
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
/>
</View>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={[styles.settingTitle, !settings.enableLocalScrapers && styles.disabledText]}>Show Scraper Logos</Text>
<Text style={[styles.settingDescription, !settings.enableLocalScrapers && styles.disabledText]}>
Display scraper logos next to streaming links on the streams screen.
</Text>
</View>
<Switch
value={settings.showScraperLogos && settings.enableLocalScrapers}
onValueChange={(value) => updateSetting('showScraperLogos', value)}
trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers}
/>
</View>
</View>
{/* Quality Filtering */}

View file

@ -62,13 +62,14 @@ const scraperLogoCache = new Map<string, string>();
let scraperLogoCachePromise: Promise<void> | 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 && (
<View style={styles.scraperLogoContainer}>
<Image
source={{ uri: scraperLogo }}
@ -1276,9 +1277,10 @@ export const StreamsScreen = () => {
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];