mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Merge branch 'recovered-ui-changes'
This commit is contained in:
commit
9619f6b0b0
4 changed files with 134 additions and 53 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in a new issue