some ui changes

This commit is contained in:
tapframe 2025-07-30 13:49:04 +05:30
parent 8cf3508e22
commit f209b538f3
5 changed files with 135 additions and 54 deletions

@ -1 +1 @@
Subproject commit d129a1d2799397738c6e4848a6b80314a3326bd9 Subproject commit b69f3233879c55439465d4669bb786f14121573a

View file

@ -711,55 +711,112 @@ const AndroidVideoPlayer: React.FC = () => {
}; };
const handleError = (error: any) => { const handleError = (error: any) => {
logger.error('AndroidVideoPlayer error: ', error); try {
logger.error('AndroidVideoPlayer error: ', error);
// Check for specific AVFoundation server configuration errors
const isServerConfigError = error?.error?.code === -11850 || // Early return if component is unmounted to prevent iOS crashes
error?.code === -11850 || if (!isMounted.current) {
(error?.error?.localizedDescription && logger.warn('[AndroidVideoPlayer] Component unmounted, skipping error handling');
error.error.localizedDescription.includes('server is not correctly configured')); return;
}
// Format error details for user display
let errorMessage = 'An unknown error occurred'; // Check for specific AVFoundation server configuration errors
if (error) { const isServerConfigError = error?.error?.code === -11850 ||
if (isServerConfigError) { error?.code === -11850 ||
errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.'; (error?.error?.localizedDescription &&
} else if (typeof error === 'string') { error.error.localizedDescription.includes('server is not correctly configured'));
errorMessage = error;
} else if (error.message) { // Format error details for user display
errorMessage = error.message; let errorMessage = 'An unknown error occurred';
} else if (error.error && error.error.message) { if (error) {
errorMessage = error.error.message; if (isServerConfigError) {
} else if (error.error && error.error.localizedDescription) { errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.';
errorMessage = error.error.localizedDescription; } else if (typeof error === 'string') {
} else if (error.code) { errorMessage = error;
errorMessage = `Error Code: ${error.code}`; } else if (error.message) {
} else { errorMessage = error.message;
errorMessage = JSON.stringify(error, null, 2); } 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 = () => { const handleErrorExit = () => {
if (errorTimeoutRef.current) { try {
clearTimeout(errorTimeoutRef.current); // Early return if component is unmounted
errorTimeoutRef.current = null; 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) => { const onBuffer = (data: any) => {
@ -1323,12 +1380,15 @@ const AndroidVideoPlayer: React.FC = () => {
/> />
{/* Error Modal */} {/* Error Modal */}
<Modal {isMounted.current && (
visible={showErrorModal} <Modal
transparent visible={showErrorModal}
animationType="fade" transparent
onRequestClose={handleErrorExit} animationType="fade"
> onRequestClose={handleErrorExit}
supportedOrientations={['landscape', 'portrait']}
statusBarTranslucent={true}
>
<View style={{ <View style={{
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
@ -1414,7 +1474,8 @@ const AndroidVideoPlayer: React.FC = () => {
}}>This dialog will auto-close in 5 seconds</Text> }}>This dialog will auto-close in 5 seconds</Text>
</View> </View>
</View> </View>
</Modal> </Modal>
)}
</View> </View>
); );
}; };

View file

@ -43,6 +43,7 @@ export interface AppSettings {
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name 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 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 // Quality filtering settings
excludedQualities: string[]; // Array of quality strings to exclude (e.g., ['2160p', '4K', '1080p', '720p']) 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 enableScraperUrlValidation: true, // Enable URL validation by default
streamDisplayMode: 'separate', // Default to separate display by provider streamDisplayMode: 'separate', // Default to separate display by provider
streamSortMode: 'scraper-then-quality', // Default to current behavior (scraper first, then quality) streamSortMode: 'scraper-then-quality', // Default to current behavior (scraper first, then quality)
showScraperLogos: true, // Show scraper logos by default
// Quality filtering defaults // Quality filtering defaults
excludedQualities: [], // No qualities excluded by default excludedQualities: [], // No qualities excluded by default
}; };

View file

@ -824,6 +824,22 @@ const PluginsScreen: React.FC = () => {
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'} disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
/> />
</View> </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> </View>
{/* Quality Filtering */} {/* Quality Filtering */}

View file

@ -62,13 +62,14 @@ const scraperLogoCache = new Map<string, string>();
let scraperLogoCachePromise: Promise<void> | null = null; let scraperLogoCachePromise: Promise<void> | null = null;
// Extracted Components // Extracted Components
const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme }: { const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos }: {
stream: Stream; stream: Stream;
onPress: () => void; onPress: () => void;
index: number; index: number;
isLoading?: boolean; isLoading?: boolean;
statusMessage?: string; statusMessage?: string;
theme: any; theme: any;
showLogos?: boolean;
}) => { }) => {
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); 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} activeOpacity={0.7}
> >
{/* Scraper Logo */} {/* Scraper Logo */}
{scraperLogo && ( {showLogos && scraperLogo && (
<View style={styles.scraperLogoContainer}> <View style={styles.scraperLogoContainer}>
<Image <Image
source={{ uri: scraperLogo }} source={{ uri: scraperLogo }}
@ -1276,9 +1277,10 @@ export const StreamsScreen = () => {
isLoading={isLoading} isLoading={isLoading}
statusMessage={undefined} statusMessage={undefined}
theme={currentTheme} theme={currentTheme}
showLogos={settings.showScraperLogos}
/> />
); );
}, [handleStreamPress, currentTheme]); }, [handleStreamPress, currentTheme, settings.showScraperLogos]);
const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => { const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => {
const isProviderLoading = loadingProviders[section.addonId]; const isProviderLoading = loadingProviders[section.addonId];