import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, Switch, TextInput, ScrollView, RefreshControl, StatusBar, Platform, Image, ActivityIndicator, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; import { useSettings } from '../hooks/useSettings'; import { localScraperService, ScraperInfo } from '../services/localScraperService'; import { logger } from '../utils/logger'; import { useTheme } from '../contexts/ThemeContext'; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; // Create a styles creator function that accepts the theme colors const createStyles = (colors: any) => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background, }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingTop: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16, paddingBottom: 16, }, backButton: { flexDirection: 'row', alignItems: 'center', }, backText: { fontSize: 17, color: colors.primary, marginLeft: 8, }, headerTitle: { fontSize: 34, fontWeight: 'bold', color: colors.white, paddingHorizontal: 16, marginBottom: 24, }, scrollView: { flex: 1, }, section: { backgroundColor: colors.elevation1, marginBottom: 16, borderRadius: 12, padding: 16, }, sectionTitle: { fontSize: 20, fontWeight: '600', color: colors.white, marginBottom: 8, }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, sectionDescription: { fontSize: 14, color: colors.mediumGray, marginBottom: 16, lineHeight: 20, }, emptyContainer: { backgroundColor: colors.elevation2, borderRadius: 12, padding: 32, alignItems: 'center', justifyContent: 'center', marginBottom: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, }, emptyText: { marginTop: 8, color: colors.mediumGray, fontSize: 15, }, scraperItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.elevation2, padding: 12, marginBottom: 8, borderRadius: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1, }, scraperLogo: { width: 40, height: 40, marginRight: 12, borderRadius: 6, backgroundColor: colors.elevation3, }, scraperInfo: { flex: 1, }, scraperName: { fontSize: 15, fontWeight: '600', color: colors.white, marginBottom: 2, }, scraperDescription: { fontSize: 13, color: colors.mediumGray, marginBottom: 4, lineHeight: 18, }, scraperMeta: { flexDirection: 'row', alignItems: 'center', }, scraperVersion: { fontSize: 12, color: colors.mediumGray, }, scraperDot: { fontSize: 12, color: colors.mediumGray, marginHorizontal: 8, }, scraperTypes: { fontSize: 12, color: colors.mediumGray, }, scraperLanguage: { fontSize: 12, color: colors.mediumGray, }, settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, settingInfo: { flex: 1, marginRight: 16, }, settingTitle: { fontSize: 17, fontWeight: '600', color: colors.white, marginBottom: 2, }, settingDescription: { fontSize: 14, color: colors.mediumEmphasis, lineHeight: 20, }, textInput: { backgroundColor: colors.elevation1, borderRadius: 8, padding: 12, color: colors.white, marginBottom: 16, fontSize: 15, }, button: { backgroundColor: colors.elevation2, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 8, marginRight: 8, }, primaryButton: { backgroundColor: colors.primary, }, secondaryButton: { backgroundColor: colors.elevation2, }, buttonText: { fontSize: 16, fontWeight: '600', color: colors.white, textAlign: 'center', }, secondaryButtonText: { fontSize: 16, fontWeight: '600', color: colors.mediumGray, textAlign: 'center', }, clearButton: { backgroundColor: '#ff3b30', paddingVertical: 8, paddingHorizontal: 12, borderRadius: 8, }, clearButtonText: { fontSize: 14, fontWeight: '600', color: colors.white, }, currentRepoContainer: { backgroundColor: colors.elevation1, borderRadius: 8, padding: 12, marginBottom: 16, }, currentRepoLabel: { fontSize: 14, fontWeight: '600', color: colors.primary, marginBottom: 4, }, currentRepoUrl: { fontSize: 14, color: colors.white, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', lineHeight: 18, }, urlHint: { fontSize: 12, color: colors.mediumGray, marginBottom: 8, lineHeight: 16, }, defaultRepoButton: { backgroundColor: colors.elevation3, borderRadius: 6, paddingVertical: 8, paddingHorizontal: 12, marginBottom: 16, alignItems: 'center', }, defaultRepoButtonText: { color: colors.primary, fontSize: 14, fontWeight: '500', }, buttonRow: { flexDirection: 'row', gap: 12, }, infoText: { fontSize: 14, color: colors.mediumEmphasis, lineHeight: 20, }, content: { flex: 1, }, emptyState: { alignItems: 'center', paddingVertical: 32, }, emptyStateTitle: { fontSize: 18, fontWeight: '600', color: colors.white, marginTop: 16, marginBottom: 8, }, emptyStateDescription: { fontSize: 14, color: colors.mediumGray, textAlign: 'center', lineHeight: 20, }, scrapersList: { gap: 12, }, scrapersContainer: { marginBottom: 24, }, inputContainer: { marginBottom: 16, }, lastSection: { borderBottomWidth: 0, }, disabledSection: { opacity: 0.5, }, disabledText: { color: colors.elevation3, }, disabledContainer: { opacity: 0.5, }, disabledInput: { backgroundColor: colors.elevation1, opacity: 0.5, }, disabledButton: { opacity: 0.5, }, disabledImage: { opacity: 0.3, }, }); const ScraperSettingsScreen: React.FC = () => { const navigation = useNavigation(); const { settings, updateSetting } = useSettings(); const { currentTheme } = useTheme(); const colors = currentTheme.colors; const styles = createStyles(colors); const [repositoryUrl, setRepositoryUrl] = useState(settings.scraperRepositoryUrl); const [installedScrapers, setInstalledScrapers] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [hasRepository, setHasRepository] = useState(false); useEffect(() => { loadScrapers(); checkRepository(); }, []); const loadScrapers = async () => { try { const scrapers = await localScraperService.getInstalledScrapers(); setInstalledScrapers(scrapers); } catch (error) { logger.error('[ScraperSettings] Failed to load scrapers:', error); } }; const checkRepository = async () => { try { const repoUrl = await localScraperService.getRepositoryUrl(); setHasRepository(!!repoUrl); if (repoUrl && repoUrl !== repositoryUrl) { setRepositoryUrl(repoUrl); } } catch (error) { logger.error('[ScraperSettings] Failed to check repository:', error); } }; const handleSaveRepository = async () => { if (!repositoryUrl.trim()) { Alert.alert('Error', 'Please enter a valid repository URL'); return; } // Validate URL format const url = repositoryUrl.trim(); if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { Alert.alert( 'Invalid URL Format', 'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/branch/\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/main/' ); return; } try { setIsLoading(true); await localScraperService.setRepositoryUrl(url); await updateSetting('scraperRepositoryUrl', url); setHasRepository(true); Alert.alert('Success', 'Repository URL saved successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to save repository:', error); Alert.alert('Error', 'Failed to save repository URL'); } finally { setIsLoading(false); } }; const handleRefreshRepository = async () => { if (!repositoryUrl.trim()) { Alert.alert('Error', 'Please set a repository URL first'); return; } try { setIsRefreshing(true); await localScraperService.refreshRepository(); await loadScrapers(); Alert.alert('Success', 'Repository refreshed successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to refresh repository:', error); const errorMessage = error instanceof Error ? error.message : String(error); Alert.alert( 'Repository Error', `Failed to refresh repository: ${errorMessage}\n\nPlease ensure your URL is correct and follows this format:\nhttps://raw.githubusercontent.com/username/repo/branch/` ); } finally { setIsRefreshing(false); } }; const handleToggleScraper = async (scraperId: string, enabled: boolean) => { try { await localScraperService.setScraperEnabled(scraperId, enabled); await loadScrapers(); } catch (error) { logger.error('[ScraperSettings] Failed to toggle scraper:', error); Alert.alert('Error', 'Failed to update scraper status'); } }; const handleClearScrapers = () => { Alert.alert( 'Clear All Scrapers', 'Are you sure you want to remove all installed scrapers? This action cannot be undone.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Clear', style: 'destructive', onPress: async () => { try { await localScraperService.clearScrapers(); await loadScrapers(); Alert.alert('Success', 'All scrapers have been removed'); } catch (error) { logger.error('[ScraperSettings] Failed to clear scrapers:', error); Alert.alert('Error', 'Failed to clear scrapers'); } }, }, ] ); }; const handleClearCache = () => { Alert.alert( 'Clear Repository Cache', 'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Clear Cache', style: 'destructive', onPress: async () => { try { await localScraperService.clearScrapers(); await localScraperService.setRepositoryUrl(''); await updateSetting('scraperRepositoryUrl', ''); setRepositoryUrl(''); setHasRepository(false); await loadScrapers(); Alert.alert('Success', 'Repository cache cleared successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to clear cache:', error); Alert.alert('Error', 'Failed to clear repository cache'); } }, }, ] ); }; const handleUseDefaultRepo = () => { const defaultUrl = 'https://raw.githubusercontent.com/tapframe/nuvio-providers/main'; setRepositoryUrl(defaultUrl); }; const handleToggleLocalScrapers = async (enabled: boolean) => { await updateSetting('enableLocalScrapers', enabled); }; const handleToggleUrlValidation = async (enabled: boolean) => { await updateSetting('enableScraperUrlValidation', enabled); }; return ( navigation.goBack()} > Settings Local Scrapers } > {/* Enable Local Scrapers - Top Priority */} Enable Local Scrapers Allow the app to use locally installed scrapers for finding streams {/* Repository Configuration - Moved up for better UX */} Repository Configuration {hasRepository && settings.enableLocalScrapers && ( Clear Cache )} Enter the URL of a Nuvio scraper repository to download and install scrapers. {hasRepository && repositoryUrl && ( Current Repository: {repositoryUrl} )} 💡 Use GitHub raw URL format. Default: https://raw.githubusercontent.com/tapframe/nuvio-providers/main Use Default Repository {isLoading ? ( ) : ( Save Repository )} {hasRepository && ( {isRefreshing ? ( ) : ( Refresh )} )} {/* Installed Scrapers */} Installed Scrapers {installedScrapers.length > 0 && settings.enableLocalScrapers && ( Clear All )} {installedScrapers.length === 0 ? ( No Scrapers Installed Configure a repository above to install scrapers. ) : ( {installedScrapers.map((scraper) => ( {scraper.logo ? ( ) : ( )} {scraper.name} {scraper.description} v{scraper.version} • {scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'} {scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && ( <> • {scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} )} handleToggleScraper(scraper.id, enabled)} trackColor={{ false: colors.elevation3, true: colors.primary }} thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} disabled={!settings.enableLocalScrapers} /> ))} )} {/* Additional Scraper Settings */} Additional Settings Enable URL Validation Validate streaming URLs before returning them (may slow down results but improves reliability) {/* About */} About Local Scrapers Local scrapers are JavaScript modules that can search for streaming links from various sources. They run locally on your device and can be installed from trusted repositories. ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000000', }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#333', }, backButton: { marginRight: 16, }, headerTitle: { fontSize: 20, fontWeight: '600', color: '#ffffff', }, content: { flex: 1, }, section: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#333', }, lastSection: { borderBottomWidth: 0, }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, marginHorizontal: -16, paddingHorizontal: 16, }, sectionTitle: { fontSize: 18, fontWeight: '600', color: '#ffffff', marginBottom: 8, }, sectionDescription: { fontSize: 14, color: '#999', marginBottom: 16, lineHeight: 20, }, settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, settingInfo: { flex: 1, marginRight: 16, }, settingTitle: { fontSize: 16, fontWeight: '500', color: '#ffffff', marginBottom: 4, }, settingDescription: { fontSize: 14, color: '#999', lineHeight: 18, }, inputContainer: { marginBottom: 16, }, textInput: { backgroundColor: '#1a1a1a', borderRadius: 8, padding: 12, fontSize: 16, color: '#ffffff', borderWidth: 1, borderColor: '#333', }, buttonRow: { flexDirection: 'row', gap: 12, }, button: { flex: 1, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', minHeight: 44, }, primaryButton: { backgroundColor: '#007AFF', }, secondaryButton: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#007AFF', }, buttonText: { color: '#ffffff', fontSize: 16, fontWeight: '600', }, secondaryButtonText: { color: '#007AFF', fontSize: 16, fontWeight: '600', }, clearButton: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: 6, backgroundColor: '#ff3b30', marginLeft: 0, }, clearButtonText: { color: '#ffffff', fontSize: 14, fontWeight: '500', }, scrapersList: { gap: 12, }, scraperItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#1a1a1a', borderRadius: 8, padding: 16, borderWidth: 1, borderColor: '#333', }, scraperLogo: { width: 40, height: 40, marginRight: 12, borderRadius: 8, }, scraperInfo: { flex: 1, marginRight: 16, }, scraperName: { fontSize: 16, fontWeight: '600', color: '#ffffff', marginBottom: 4, }, scraperDescription: { fontSize: 14, color: '#999', marginBottom: 8, lineHeight: 18, }, scraperMeta: { flexDirection: 'row', gap: 12, }, scraperVersion: { fontSize: 12, color: '#007AFF', fontWeight: '500', }, scraperTypes: { fontSize: 12, color: '#666', textTransform: 'uppercase', }, emptyState: { alignItems: 'center', paddingVertical: 32, }, emptyStateTitle: { fontSize: 18, fontWeight: '600', color: '#ffffff', marginTop: 16, marginBottom: 8, }, emptyStateDescription: { fontSize: 14, color: '#999', textAlign: 'center', lineHeight: 20, }, infoText: { fontSize: 14, color: '#999', lineHeight: 20, marginBottom: 12, }, currentRepoContainer: { backgroundColor: '#1a1a1a', borderRadius: 8, padding: 12, marginBottom: 16, borderWidth: 1, borderColor: '#333', }, currentRepoLabel: { fontSize: 14, fontWeight: '500', color: '#007AFF', marginBottom: 4, }, currentRepoUrl: { fontSize: 14, color: '#ffffff', fontFamily: 'monospace', lineHeight: 18, }, urlHint: { fontSize: 12, color: '#666', marginTop: 8, lineHeight: 16, }, defaultRepoButton: { backgroundColor: '#333', borderRadius: 6, paddingVertical: 8, paddingHorizontal: 12, marginTop: 8, alignItems: 'center', }, defaultRepoButtonText: { color: '#007AFF', fontSize: 14, fontWeight: '500', }, }); export default ScraperSettingsScreen;