This commit is contained in:
tapframe 2025-07-27 18:27:58 +05:30
parent 5bb3aa7e3b
commit ae66ec1c23

View file

@ -3,14 +3,16 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
ScrollView,
TextInput,
TouchableOpacity, TouchableOpacity,
Alert, Alert,
Switch, Switch,
ActivityIndicator, TextInput,
ScrollView,
RefreshControl, RefreshControl,
StatusBar,
Platform,
Image, Image,
ActivityIndicator,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
@ -18,10 +20,309 @@ import { useNavigation } from '@react-navigation/native';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { localScraperService, ScraperInfo } from '../services/localScraperService'; import { localScraperService, ScraperInfo } from '../services/localScraperService';
import { logger } from '../utils/logger'; 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,
paddingHorizontal: 0,
},
sectionDescription: {
fontSize: 14,
color: colors.mediumGray,
marginBottom: 16,
lineHeight: 20,
},
emptyContainer: {
backgroundColor: colors.elevation2,
borderRadius: 12,
padding: 32,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 16,
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,
marginHorizontal: 16,
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,
},
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 ScraperSettingsScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
const colors = currentTheme.colors;
const styles = createStyles(colors);
const [repositoryUrl, setRepositoryUrl] = useState(settings.scraperRepositoryUrl); const [repositoryUrl, setRepositoryUrl] = useState(settings.scraperRepositoryUrl);
const [installedScrapers, setInstalledScrapers] = useState<ScraperInfo[]>([]); const [installedScrapers, setInstalledScrapers] = useState<ScraperInfo[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -182,53 +483,33 @@ const ScraperSettingsScreen: React.FC = () => {
await updateSetting('enableScraperUrlValidation', enabled); await updateSetting('enableScraperUrlValidation', enabled);
}; };
const renderScraperItem = (scraper: ScraperInfo) => (
<View key={scraper.id} style={styles.scraperItem}>
{scraper.logo && (
<Image
source={{ uri: scraper.logo }}
style={styles.scraperLogo}
resizeMode="contain"
/>
)}
<View style={styles.scraperInfo}>
<Text style={styles.scraperName}>{scraper.name}</Text>
<Text style={styles.scraperDescription}>{scraper.description}</Text>
<View style={styles.scraperMeta}>
<Text style={styles.scraperVersion}>v{scraper.version}</Text>
<Text style={styles.scraperTypes}>
{scraper.supportedTypes.join(', ')}
</Text>
</View>
</View>
<Switch
value={scraper.enabled}
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
trackColor={{ false: '#767577', true: '#007AFF' }}
thumbColor={scraper.enabled ? '#ffffff' : '#f4f3f4'}
/>
</View>
);
return ( return (
<SafeAreaView style={styles.container}> <View style={styles.container}>
<StatusBar
barStyle={Platform.OS === 'ios' ? 'light-content' : 'light-content'}
backgroundColor={colors.background}
/>
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Ionicons name="arrow-back" size={24} color="#007AFF" /> <Ionicons name="arrow-back" size={24} color={colors.primary} />
<Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.headerTitle}>Local Scrapers</Text>
</View> </View>
<Text style={styles.headerTitle}>Local Scrapers</Text>
<ScrollView <ScrollView
style={styles.content} style={styles.scrollView}
refreshControl={ refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={loadScrapers} /> <RefreshControl refreshing={isRefreshing} onRefresh={loadScrapers} />
} }
> >
{/* Enable/Disable Local Scrapers */} {/* Enable Local Scrapers - Top Priority */}
<View style={styles.section}> <View style={styles.section}>
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingInfo}> <View style={styles.settingInfo}>
@ -240,33 +521,17 @@ const ScraperSettingsScreen: React.FC = () => {
<Switch <Switch
value={settings.enableLocalScrapers} value={settings.enableLocalScrapers}
onValueChange={handleToggleLocalScrapers} onValueChange={handleToggleLocalScrapers}
trackColor={{ false: '#767577', true: '#007AFF' }} trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.enableLocalScrapers ? '#ffffff' : '#f4f3f4'} thumbColor={settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
/>
</View>
{/* URL Validation Toggle */}
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Enable URL Validation</Text>
<Text style={styles.settingDescription}>
Validate streaming URLs before returning them (may slow down results but improves reliability)
</Text>
</View>
<Switch
value={settings.enableScraperUrlValidation}
onValueChange={handleToggleUrlValidation}
trackColor={{ false: '#767577', true: '#007AFF' }}
thumbColor={settings.enableScraperUrlValidation ? '#ffffff' : '#f4f3f4'}
/> />
</View> </View>
</View> </View>
{/* Repository Configuration */} {/* Repository Configuration - Moved up for better UX */}
<View style={styles.section}> <View style={[styles.section, !settings.enableLocalScrapers && styles.disabledSection]}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Repository Configuration</Text> <Text style={[styles.sectionTitle, !settings.enableLocalScrapers && styles.disabledText]}>Repository Configuration</Text>
{hasRepository && ( {hasRepository && settings.enableLocalScrapers && (
<TouchableOpacity <TouchableOpacity
style={styles.clearButton} style={styles.clearButton}
onPress={handleClearCache} onPress={handleClearCache}
@ -275,63 +540,65 @@ const ScraperSettingsScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<Text style={styles.sectionDescription}> <Text style={[styles.sectionDescription, !settings.enableLocalScrapers && styles.disabledText]}>
Enter the URL of a Nuvio scraper repository to download and install scrapers. Enter the URL of a Nuvio scraper repository to download and install scrapers.
</Text> </Text>
{hasRepository && repositoryUrl && ( {hasRepository && repositoryUrl && (
<View style={styles.currentRepoContainer}> <View style={[styles.currentRepoContainer, !settings.enableLocalScrapers && styles.disabledContainer]}>
<Text style={styles.currentRepoLabel}>Current Repository:</Text> <Text style={[styles.currentRepoLabel, !settings.enableLocalScrapers && styles.disabledText]}>Current Repository:</Text>
<Text style={styles.currentRepoUrl}>{repositoryUrl}</Text> <Text style={[styles.currentRepoUrl, !settings.enableLocalScrapers && styles.disabledText]}>{repositoryUrl}</Text>
</View> </View>
)} )}
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
<TextInput <TextInput
style={styles.textInput} style={[styles.textInput, !settings.enableLocalScrapers && styles.disabledInput]}
value={repositoryUrl} value={repositoryUrl}
onChangeText={setRepositoryUrl} onChangeText={setRepositoryUrl}
placeholder="https://raw.githubusercontent.com/tapframe/nuvio-providers/main" placeholder="https://raw.githubusercontent.com/tapframe/nuvio-providers/main"
placeholderTextColor="#999" placeholderTextColor={!settings.enableLocalScrapers ? colors.elevation3 : "#999"}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
keyboardType="url" keyboardType="url"
editable={settings.enableLocalScrapers}
/> />
<Text style={styles.urlHint}> <Text style={[styles.urlHint, !settings.enableLocalScrapers && styles.disabledText]}>
💡 Use GitHub raw URL format. Default: https://raw.githubusercontent.com/tapframe/nuvio-providers/main 💡 Use GitHub raw URL format. Default: https://raw.githubusercontent.com/tapframe/nuvio-providers/main
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={styles.defaultRepoButton} style={[styles.defaultRepoButton, !settings.enableLocalScrapers && styles.disabledButton]}
onPress={handleUseDefaultRepo} onPress={handleUseDefaultRepo}
disabled={!settings.enableLocalScrapers}
> >
<Text style={styles.defaultRepoButtonText}>Use Default Repository</Text> <Text style={[styles.defaultRepoButtonText, !settings.enableLocalScrapers && styles.disabledText]}>Use Default Repository</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
<TouchableOpacity <TouchableOpacity
style={[styles.button, styles.primaryButton]} style={[styles.button, styles.primaryButton, !settings.enableLocalScrapers && styles.disabledButton]}
onPress={handleSaveRepository} onPress={handleSaveRepository}
disabled={isLoading} disabled={isLoading || !settings.enableLocalScrapers}
> >
{isLoading ? ( {isLoading ? (
<ActivityIndicator size="small" color="#ffffff" /> <ActivityIndicator size="small" color="#ffffff" />
) : ( ) : (
<Text style={styles.buttonText}>Save Repository</Text> <Text style={[styles.buttonText, !settings.enableLocalScrapers && styles.disabledText]}>Save Repository</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
{hasRepository && ( {hasRepository && (
<TouchableOpacity <TouchableOpacity
style={[styles.button, styles.secondaryButton]} style={[styles.button, styles.secondaryButton, !settings.enableLocalScrapers && styles.disabledButton]}
onPress={handleRefreshRepository} onPress={handleRefreshRepository}
disabled={isRefreshing} disabled={isRefreshing || !settings.enableLocalScrapers}
> >
{isRefreshing ? ( {isRefreshing ? (
<ActivityIndicator size="small" color="#007AFF" /> <ActivityIndicator size="small" color={colors.primary} />
) : ( ) : (
<Text style={styles.secondaryButtonText}>Refresh</Text> <Text style={[styles.secondaryButtonText, !settings.enableLocalScrapers && styles.disabledText]}>Refresh</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
)} )}
@ -339,10 +606,10 @@ const ScraperSettingsScreen: React.FC = () => {
</View> </View>
{/* Installed Scrapers */} {/* Installed Scrapers */}
<View style={styles.section}> <View style={[!settings.enableLocalScrapers && styles.disabledSection]}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Installed Scrapers</Text> <Text style={[styles.sectionTitle, !settings.enableLocalScrapers && styles.disabledText]}>Installed Scrapers</Text>
{installedScrapers.length > 0 && ( {installedScrapers.length > 0 && settings.enableLocalScrapers && (
<TouchableOpacity <TouchableOpacity
style={styles.clearButton} style={styles.clearButton}
onPress={handleClearScrapers} onPress={handleClearScrapers}
@ -353,33 +620,81 @@ const ScraperSettingsScreen: React.FC = () => {
</View> </View>
{installedScrapers.length === 0 ? ( {installedScrapers.length === 0 ? (
<View style={styles.emptyState}> <View style={[styles.emptyContainer, !settings.enableLocalScrapers && styles.disabledContainer]}>
<Ionicons name="download-outline" size={48} color="#999" /> <Ionicons name="download-outline" size={48} color={!settings.enableLocalScrapers ? colors.elevation3 : colors.mediumGray} />
<Text style={styles.emptyStateTitle}>No Scrapers Installed</Text> <Text style={[styles.emptyStateTitle, !settings.enableLocalScrapers && styles.disabledText]}>No Scrapers Installed</Text>
<Text style={styles.emptyStateDescription}> <Text style={[styles.emptyStateDescription, !settings.enableLocalScrapers && styles.disabledText]}>
Add a repository URL above and refresh to install scrapers. Configure a repository above to install scrapers.
</Text> </Text>
</View> </View>
) : ( ) : (
<View style={styles.scrapersList}> <View style={styles.scrapersContainer}>
{installedScrapers.map(renderScraperItem)} {installedScrapers.map((scraper) => (
</View> <View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}>
)} {scraper.logo ? (
<Image
source={{ uri: scraper.logo }}
style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]}
resizeMode="contain"
/>
) : (
<View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} />
)}
<View style={styles.scraperInfo}>
<Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text>
<Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text>
<View style={styles.scraperMeta}>
<Text style={[styles.scraperVersion, !settings.enableLocalScrapers && styles.disabledText]}>v{scraper.version}</Text>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text>
<Text style={[styles.scraperTypes, !settings.enableLocalScrapers && styles.disabledText]}>
{scraper.supportedTypes.join(', ')}
</Text>
</View>
</View>
<Switch
value={scraper.enabled && settings.enableLocalScrapers}
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers}
/>
</View>
))}
</View>
)}
</View> </View>
{/* Information */} {/* Additional Scraper Settings */}
<View style={styles.section}> <View style={[styles.section, !settings.enableLocalScrapers && styles.disabledSection]}>
<Text style={[styles.sectionTitle, !settings.enableLocalScrapers && styles.disabledText]}>Additional Settings</Text>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={[styles.settingTitle, !settings.enableLocalScrapers && styles.disabledText]}>Enable URL Validation</Text>
<Text style={[styles.settingDescription, !settings.enableLocalScrapers && styles.disabledText]}>
Validate streaming URLs before returning them (may slow down results but improves reliability)
</Text>
</View>
<Switch
value={settings.enableScraperUrlValidation && settings.enableLocalScrapers}
onValueChange={handleToggleUrlValidation}
trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.enableScraperUrlValidation && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers}
/>
</View>
</View>
{/* About */}
<View style={[styles.section, styles.lastSection]}>
<Text style={styles.sectionTitle}>About Local Scrapers</Text> <Text style={styles.sectionTitle}>About Local Scrapers</Text>
<Text style={styles.infoText}> <Text style={styles.infoText}>
Local scrapers are JavaScript modules that can search for streaming links from various sources. 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. They run locally on your device and can be installed from trusted repositories.
</Text> </Text>
<Text style={styles.infoText}>
Only install scrapers from trusted sources. Malicious scrapers could potentially access your data.
</Text>
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </View>
); );
}; };
@ -412,11 +727,16 @@ const styles = StyleSheet.create({
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#333', borderBottomColor: '#333',
}, },
lastSection: {
borderBottomWidth: 0,
},
sectionHeader: { sectionHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: 12,
marginHorizontal: -16,
paddingHorizontal: 16,
}, },
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 18,
@ -498,6 +818,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 12, paddingHorizontal: 12,
borderRadius: 6, borderRadius: 6,
backgroundColor: '#ff3b30', backgroundColor: '#ff3b30',
marginLeft: 0,
}, },
clearButtonText: { clearButtonText: {
color: '#ffffff', color: '#ffffff',