import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, Switch, TextInput, ScrollView, RefreshControl, StatusBar, Platform, Image, ActivityIndicator, Modal, Dimensions, Animated, } 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, RepositoryInfo } from '../services/localScraperService'; import { logger } from '../utils/logger'; import { useTheme } from '../contexts/ThemeContext'; const { width: screenWidth } = Dimensions.get('window'); 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: 'transparent', paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, borderWidth: 1, borderColor: colors.elevation3, alignItems: 'center', justifyContent: 'center', minHeight: 44, }, primaryButton: { backgroundColor: colors.primary, borderColor: colors.primary, }, secondaryButton: { backgroundColor: 'transparent', borderColor: colors.elevation3, }, buttonText: { fontSize: 15, fontWeight: '500', color: colors.white, textAlign: 'center', }, secondaryButtonText: { fontSize: 15, fontWeight: '500', color: colors.white, 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, marginTop: 8, }, 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, }, availableIndicator: { backgroundColor: colors.primary, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, marginLeft: 8, }, availableIndicatorText: { color: colors.white, fontSize: 10, fontWeight: '600', }, qualityChipsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 8, }, qualityChip: { backgroundColor: colors.elevation2, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, borderWidth: 1, borderColor: colors.elevation3, }, qualityChipSelected: { backgroundColor: '#ff3b30', borderColor: '#ff3b30', }, qualityChipText: { color: colors.white, fontSize: 13, fontWeight: '500', }, qualityChipTextSelected: { color: colors.white, fontWeight: '600', }, // New styles for improved UX collapsibleSection: { backgroundColor: colors.elevation1, marginBottom: 16, borderRadius: 12, overflow: 'hidden', }, collapsibleHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, backgroundColor: colors.elevation2, }, collapsibleTitle: { fontSize: 18, fontWeight: '600', color: colors.white, }, collapsibleContent: { padding: 16, }, searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.elevation1, borderRadius: 12, marginBottom: 16, paddingHorizontal: 12, }, searchInput: { flex: 1, paddingVertical: 12, paddingHorizontal: 8, color: colors.white, fontSize: 16, }, filterContainer: { flexDirection: 'row', marginBottom: 16, gap: 8, }, filterChip: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: colors.elevation2, borderWidth: 1, borderColor: colors.elevation3, }, filterChipSelected: { backgroundColor: colors.primary, borderColor: colors.primary, }, filterChipText: { color: colors.white, fontSize: 14, fontWeight: '500', }, filterChipTextSelected: { color: colors.white, fontWeight: '600', }, statusBadge: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, gap: 4, }, statusBadgeText: { color: 'white', fontSize: 11, fontWeight: '600', }, bulkActionsContainer: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16, gap: 12, }, bulkActionButton: { flex: 1, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', minHeight: 44, borderWidth: 1, }, bulkActionButtonEnabled: { backgroundColor: 'transparent', borderColor: '#34C759', }, bulkActionButtonDisabled: { backgroundColor: 'transparent', borderColor: colors.elevation3, }, bulkActionButtonText: { color: colors.white, fontSize: 14, fontWeight: '500', }, helpButton: { position: 'absolute', top: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16, right: 16, backgroundColor: 'transparent', borderRadius: 20, padding: 8, borderWidth: 1, borderColor: colors.elevation3, }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', }, modalContent: { backgroundColor: colors.elevation1, borderRadius: 16, padding: 24, margin: 20, maxHeight: '80%', width: screenWidth - 40, }, modalTitle: { fontSize: 20, fontWeight: '600', color: colors.white, marginBottom: 16, }, modalText: { fontSize: 16, color: colors.mediumGray, lineHeight: 24, marginBottom: 16, }, modalButton: { backgroundColor: colors.primary, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 8, alignItems: 'center', justifyContent: 'center', marginTop: 16, minHeight: 48, }, modalButtonText: { color: colors.white, fontSize: 16, fontWeight: '500', }, quickSetupContainer: { backgroundColor: colors.elevation2, borderRadius: 12, padding: 16, marginBottom: 16, borderLeftWidth: 4, borderLeftColor: colors.primary, }, quickSetupTitle: { fontSize: 16, fontWeight: '600', color: colors.white, marginBottom: 8, }, quickSetupText: { fontSize: 14, color: colors.mediumGray, lineHeight: 20, marginBottom: 12, }, quickSetupButton: { backgroundColor: colors.primary, paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, alignItems: 'center', justifyContent: 'center', minHeight: 44, }, quickSetupButtonText: { color: colors.white, fontSize: 15, fontWeight: '500', }, scraperCard: { backgroundColor: colors.elevation2, borderRadius: 12, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: colors.elevation3, }, scraperCardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, scraperCardInfo: { flex: 1, marginRight: 12, }, scraperCardMeta: { flexDirection: 'row', alignItems: 'center', marginTop: 8, gap: 12, }, scraperCardMetaItem: { flexDirection: 'row', alignItems: 'center', gap: 4, }, scraperCardMetaText: { fontSize: 12, color: colors.mediumGray, }, emptyStateContainer: { alignItems: 'center', paddingVertical: 40, paddingHorizontal: 20, }, emptyStateIcon: { marginBottom: 16, }, // Repository management styles repositoriesList: { marginBottom: 16, }, repositoryItem: { backgroundColor: colors.elevation2, borderRadius: 12, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: colors.elevation3, }, repositoryInfo: { flex: 1, marginBottom: 12, }, repositoryName: { fontSize: 16, fontWeight: '600', color: colors.white, marginRight: 8, }, repositoryDescription: { fontSize: 14, color: colors.mediumGray, marginBottom: 4, lineHeight: 18, }, repositoryUrl: { fontSize: 12, color: colors.mediumGray, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', marginBottom: 4, }, repositoryMeta: { fontSize: 12, color: colors.mediumGray, }, repositoryActions: { flexDirection: 'row', gap: 12, marginTop: 8, }, repositoryActionButton: { paddingVertical: 8, paddingHorizontal: 16, borderRadius: 6, borderWidth: 1, alignItems: 'center', justifyContent: 'center', minHeight: 36, }, repositoryActionButtonPrimary: { backgroundColor: 'transparent', borderColor: colors.primary, }, repositoryActionButtonSecondary: { backgroundColor: 'transparent', borderColor: colors.elevation3, }, repositoryActionButtonDanger: { backgroundColor: 'transparent', borderColor: '#ff3b30', }, repositoryActionButtonText: { fontSize: 13, fontWeight: '500', color: colors.white, }, }); // Helper component for collapsible sections const CollapsibleSection: React.FC<{ title: string; children: React.ReactNode; isExpanded: boolean; onToggle: () => void; colors: any; styles: any; }> = ({ title, children, isExpanded, onToggle, colors, styles }) => ( {title} {isExpanded && {children}} ); // Helper component for info tooltips const InfoTooltip: React.FC<{ text: string; colors: any }> = ({ text, colors }) => ( ); // Helper component for status badges const StatusBadge: React.FC<{ status: 'enabled' | 'disabled' | 'available' | 'platform-disabled' | 'error'; colors: any; }> = ({ status, colors }) => { const getStatusConfig = () => { switch (status) { case 'enabled': return { color: '#34C759', text: 'Active', icon: 'checkmark-circle' }; case 'disabled': return { color: colors.mediumGray, text: 'Disabled', icon: 'close-circle' }; case 'available': return { color: colors.primary, text: 'Available', icon: 'download' }; case 'platform-disabled': return { color: '#FF9500', text: 'Platform Disabled', icon: 'phone-portrait' }; case 'error': return { color: '#FF3B30', text: 'Error', icon: 'warning' }; default: return { color: colors.mediumGray, text: 'Unknown', icon: 'help-circle' }; } }; const config = getStatusConfig(); return ( {config.text} ); }; const PluginsScreen: React.FC = () => { const navigation = useNavigation(); const { settings, updateSetting } = useSettings(); const { currentTheme } = useTheme(); const colors = currentTheme.colors; const styles = createStyles(colors); // Core state 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); const [showboxCookie, setShowboxCookie] = useState(''); const [showboxRegion, setShowboxRegion] = useState(''); // Multiple repositories state const [repositories, setRepositories] = useState([]); const [currentRepositoryId, setCurrentRepositoryId] = useState(''); const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false); const [newRepositoryUrl, setNewRepositoryUrl] = useState(''); const [newRepositoryName, setNewRepositoryName] = useState(''); const [newRepositoryDescription, setNewRepositoryDescription] = useState(''); const [switchingRepository, setSwitchingRepository] = useState(null); const [fetchingRepoName, setFetchingRepoName] = useState(false); // New UX state const [searchQuery, setSearchQuery] = useState(''); const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all'); const [expandedSections, setExpandedSections] = useState({ repository: true, scrapers: true, settings: false, quality: false, }); const [showHelpModal, setShowHelpModal] = useState(false); const [showRepositoryModal, setShowRepositoryModal] = useState(false); const [showScraperDetails, setShowScraperDetails] = useState(null); const regionOptions = [ { value: 'USA7', label: 'US East' }, { value: 'USA6', label: 'US West' }, { value: 'USA5', label: 'US Middle' }, { value: 'UK3', label: 'United Kingdom' }, { value: 'CA1', label: 'Canada' }, { value: 'FR1', label: 'France' }, { value: 'DE2', label: 'Germany' }, { value: 'HK1', label: 'Hong Kong' }, { value: 'IN1', label: 'India' }, { value: 'AU1', label: 'Australia' }, { value: 'SZ', label: 'China' }, ]; // Filtered scrapers based on search and filter const filteredScrapers = useMemo(() => { let filtered = installedScrapers; // Filter by search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(scraper => scraper.name.toLowerCase().includes(query) || scraper.description.toLowerCase().includes(query) || scraper.id.toLowerCase().includes(query) ); } // Filter by type if (selectedFilter !== 'all') { filtered = filtered.filter(scraper => scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv') ); } return filtered; }, [installedScrapers, searchQuery, selectedFilter]); // Helper functions const toggleSection = (section: keyof typeof expandedSections) => { setExpandedSections(prev => ({ ...prev, [section]: !prev[section] })); }; const getScraperStatus = (scraper: ScraperInfo): 'enabled' | 'disabled' | 'available' | 'platform-disabled' | 'error' => { if (scraper.manifestEnabled === false) return 'disabled'; if (scraper.disabledPlatforms?.includes(Platform.OS as 'ios' | 'android')) return 'platform-disabled'; if (scraper.enabled) return 'enabled'; return 'available'; }; const handleBulkToggle = async (enabled: boolean) => { try { setIsRefreshing(true); const promises = filteredScrapers.map(scraper => localScraperService.setScraperEnabled(scraper.id, enabled) ); await Promise.all(promises); await loadScrapers(); Alert.alert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredScrapers.length} scrapers`); } catch (error) { logger.error('[ScraperSettings] Failed to bulk toggle:', error); Alert.alert('Error', 'Failed to update scrapers'); } finally { setIsRefreshing(false); } }; const handleUrlChange = async (url: string) => { setNewRepositoryUrl(url); // Auto-populate repository name if it's empty and URL is valid if (!newRepositoryName.trim() && url.trim()) { setFetchingRepoName(true); try { // Try to fetch name from manifest first const manifestName = await localScraperService.fetchRepositoryNameFromManifest(url.trim()); setNewRepositoryName(manifestName); } catch (error) { // Fallback to URL extraction if manifest fetch fails try { const extractedName = localScraperService.extractRepositoryName(url.trim()); if (extractedName !== 'Unknown Repository') { setNewRepositoryName(extractedName); } } catch (extractError) { // Ignore errors, just don't auto-populate } } finally { setFetchingRepoName(false); } } }; const handleAddRepository = async () => { if (!newRepositoryUrl.trim()) { Alert.alert('Error', 'Please enter a valid repository URL'); return; } // Validate URL format const url = newRepositoryUrl.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); const repoId = await localScraperService.addRepository({ name: newRepositoryName.trim(), // Let the service fetch from manifest if empty url, description: newRepositoryDescription.trim(), enabled: true }); await loadRepositories(); // Switch to the new repository and refresh it await localScraperService.setCurrentRepository(repoId); await loadRepositories(); await loadScrapers(); setNewRepositoryUrl(''); setNewRepositoryName(''); setNewRepositoryDescription(''); setFetchingRepoName(false); setShowAddRepositoryModal(false); Alert.alert('Success', 'Repository added and refreshed successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to add repository:', error); Alert.alert('Error', 'Failed to add repository'); } finally { setIsLoading(false); } }; const handleSwitchRepository = async (repoId: string) => { try { setSwitchingRepository(repoId); await localScraperService.setCurrentRepository(repoId); await loadRepositories(); await loadScrapers(); Alert.alert('Success', 'Repository switched successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to switch repository:', error); Alert.alert('Error', 'Failed to switch repository'); } finally { setSwitchingRepository(null); } }; const handleRemoveRepository = async (repoId: string) => { const repo = repositories.find(r => r.id === repoId); if (!repo) return; Alert.alert( 'Remove Repository', `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Remove', style: 'destructive', onPress: async () => { try { await localScraperService.removeRepository(repoId); await loadRepositories(); await loadScrapers(); Alert.alert('Success', 'Repository removed successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to remove repository:', error); Alert.alert('Error', error instanceof Error ? error.message : 'Failed to remove repository'); } }, }, ] ); }; useEffect(() => { loadScrapers(); loadRepositories(); }, []); const loadScrapers = async () => { try { const scrapers = await localScraperService.getAvailableScrapers(); setInstalledScrapers(scrapers); // preload showbox settings if present const sb = scrapers.find(s => s.id === 'showboxog'); if (sb) { const s = await localScraperService.getScraperSettings('showboxog'); setShowboxCookie(s.cookie || ''); setShowboxRegion(s.region || ''); } } catch (error) { logger.error('[ScraperSettings] Failed to load scrapers:', error); } }; const loadRepositories = async () => { try { // First refresh repository names from manifests for existing repositories await localScraperService.refreshRepositoryNamesFromManifests(); const repos = await localScraperService.getRepositories(); setRepositories(repos); setHasRepository(repos.length > 0); const currentRepoId = localScraperService.getCurrentRepositoryId(); setCurrentRepositoryId(currentRepoId); const currentRepo = repos.find(r => r.id === currentRepoId); if (currentRepo) { setRepositoryUrl(currentRepo.url); } } catch (error) { logger.error('[ScraperSettings] Failed to load repositories:', 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(); // This will now load available scrapers from manifest 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 { if (enabled) { // If enabling a scraper, ensure it's installed first const installedScrapers = await localScraperService.getInstalledScrapers(); const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId); if (!isInstalled) { // Need to install the scraper first setIsRefreshing(true); await localScraperService.refreshRepository(); setIsRefreshing(false); } } 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'); setIsRefreshing(false); } }; 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); }; const handleToggleQualityExclusion = async (quality: string) => { const currentExcluded = settings.excludedQualities || []; const isExcluded = currentExcluded.includes(quality); let newExcluded: string[]; if (isExcluded) { // Remove from excluded list newExcluded = currentExcluded.filter(q => q !== quality); } else { // Add to excluded list newExcluded = [...currentExcluded, quality]; } await updateSetting('excludedQualities', newExcluded); }; // Define available quality options const qualityOptions = ['2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS']; return ( {/* Header */} navigation.goBack()} > Settings {/* Help Button */} setShowHelpModal(true)} > Plugins } > {/* Quick Setup for New Users */} {!hasRepository && ( Quick Setup Get started with plugins in 3 easy steps! Enable local scrapers, set up a repository, and start streaming. { setExpandedSections(prev => ({ ...prev, repository: true })); setShowHelpModal(true); }} > Get Started )} {/* Enable Local Scrapers */} toggleSection('repository')} colors={colors} styles={styles} > Enable Local Scrapers Allow the app to use locally installed scrapers for finding streams {/* Repository Configuration */} toggleSection('repository')} colors={colors} styles={styles} > Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers. {/* Current Repository */} {currentRepositoryId && ( Current Repository: {localScraperService.getRepositoryName()} {repositoryUrl} )} {/* Repository List */} {repositories.length > 0 && ( Available Repositories {repositories.map((repo) => ( {repo.name} {repo.id === currentRepositoryId && ( Active )} {switchingRepository === repo.id && ( Switching... )} {repo.description && ( {repo.description} )} {repo.url} {repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'} {repo.id !== currentRepositoryId && ( handleSwitchRepository(repo.id)} disabled={switchingRepository === repo.id} > {switchingRepository === repo.id ? ( ) : ( Switch )} )} handleRefreshRepository()} disabled={isRefreshing || switchingRepository !== null} > {isRefreshing ? ( ) : ( Refresh )} {repositories.length > 1 && ( handleRemoveRepository(repo.id)} disabled={switchingRepository !== null} > Remove )} ))} )} {/* Add Repository Button */} setShowAddRepositoryModal(true)} disabled={!settings.enableLocalScrapers || switchingRepository !== null} > Add New Repository {/* Available Scrapers */} toggleSection('scrapers')} colors={colors} styles={styles} > {installedScrapers.length > 0 && ( <> {/* Search and Filter */} {searchQuery.length > 0 && ( setSearchQuery('')}> )} {/* Filter Chips */} {['all', 'movie', 'tv'].map((filter) => ( setSelectedFilter(filter as any)} > {filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'} ))} {/* Bulk Actions */} {filteredScrapers.length > 0 && ( handleBulkToggle(true)} disabled={isRefreshing} > Enable All handleBulkToggle(false)} disabled={isRefreshing} > Disable All )} )} {filteredScrapers.length === 0 ? ( {searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'} {searchQuery ? `No scrapers match "${searchQuery}". Try a different search term.` : 'Configure a repository above to view available scrapers.' } {searchQuery && ( setSearchQuery('')} > Clear Search )} ) : ( {filteredScrapers.map((scraper) => ( {scraper.logo ? ( ) : ( )} {scraper.name} {scraper.description} handleToggleScraper(scraper.id, enabled)} trackColor={{ false: colors.elevation3, true: colors.primary }} thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))} /> v{scraper.version} {scraper.supportedTypes?.join(', ') || 'Unknown'} {scraper.contentLanguage && scraper.contentLanguage.length > 0 && ( {scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} )} {/* ShowBox Settings */} {scraper.id === 'showboxog' && settings.enableLocalScrapers && ( ShowBox Cookie Region {regionOptions.map(opt => { const selected = showboxRegion === opt.value; return ( setShowboxRegion(opt.value)} > {opt.label} ); })} { await localScraperService.setScraperSettings('showboxog', { cookie: showboxCookie, region: showboxRegion }); Alert.alert('Saved', 'ShowBox settings updated'); }} > Save { setShowboxCookie(''); setShowboxRegion(''); await localScraperService.setScraperSettings('showboxog', {}); }} > Clear )} ))} )} {/* Additional Settings */} toggleSection('settings')} colors={colors} styles={styles} > Enable URL Validation Validate streaming URLs before returning them (may slow down results but improves reliability) Group Plugin Streams When enabled, all plugin streams are grouped under "{localScraperService.getRepositoryName()}". When disabled, each plugin shows as a separate provider. { updateSetting('streamDisplayMode', value ? 'grouped' : 'separate'); // Auto-disable quality sorting when grouping is disabled if (!value && settings.streamSortMode === 'quality-then-scraper') { updateSetting('streamSortMode', 'scraper-then-quality'); } }} trackColor={{ false: colors.elevation3, true: colors.primary }} thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'} disabled={!settings.enableLocalScrapers} /> Sort by Quality First When enabled, streams are sorted by quality first, then by scraper. When disabled, streams are sorted by scraper first, then by quality. Only available when grouping is enabled. updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')} trackColor={{ false: colors.elevation3, true: colors.primary }} thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'} 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 */} toggleSection('quality')} colors={colors} styles={styles} > Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results. {qualityOptions.map((quality) => { const isExcluded = (settings.excludedQualities || []).includes(quality); return ( handleToggleQualityExclusion(quality)} disabled={!settings.enableLocalScrapers} > {isExcluded ? '✕ ' : ''}{quality} ); })} {(settings.excludedQualities || []).length > 0 && ( Excluded qualities: {(settings.excludedQualities || []).join(', ')} )} {/* About */} About Plugins Plugins 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. {/* Help Modal */} setShowHelpModal(false)} > Getting Started with Plugins 1. Enable Local Scrapers - Turn on the main switch to allow plugins 2. Add Repository - Add a GitHub raw URL or use the default repository 3. Refresh Repository - Download available scrapers from the repository 4. Enable Scrapers - Turn on the scrapers you want to use for streaming setShowHelpModal(false)} > Got it! {/* Add Repository Modal */} setShowAddRepositoryModal(false)} > Add New Repository Repository Name {fetchingRepoName && ( )} Repository URL Description (Optional) { setShowAddRepositoryModal(false); setNewRepositoryUrl(''); setNewRepositoryName(''); setNewRepositoryDescription(''); setFetchingRepoName(false); }} > Cancel {isLoading ? ( ) : ( Add Repository )} ); }; export default PluginsScreen;