revamped alert UI

This commit is contained in:
tapframe 2025-12-16 15:24:32 +05:30
parent d876b7618c
commit 59cb902658
3 changed files with 328 additions and 308 deletions

3
.gitignore vendored
View file

@ -85,4 +85,5 @@ node_modules
expofs.md expofs.md
ios/sentry.properties ios/sentry.properties
android/sentry.properties android/sentry.properties
Stremio addons refer Stremio addons refer
trakt-docs

View file

@ -15,7 +15,7 @@ import Animated, {
withTiming, withTiming,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { Portal, Dialog, Button } from 'react-native-paper'; import { Portal } from 'react-native-paper';
interface CustomAlertProps { interface CustomAlertProps {
visible: boolean; visible: boolean;
@ -40,8 +40,8 @@ export const CustomAlert = ({
}: CustomAlertProps) => { }: CustomAlertProps) => {
const opacity = useSharedValue(0); const opacity = useSharedValue(0);
const scale = useSharedValue(0.95); const scale = useSharedValue(0.95);
const isDarkMode = useColorScheme() === 'dark';
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
// Using hardcoded dark theme values to match SeriesContent modal
const themeColors = currentTheme.colors; const themeColors = currentTheme.colors;
useEffect(() => { useEffect(() => {
@ -68,10 +68,11 @@ export const CustomAlert = ({
const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => { const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => {
try { try {
action.onPress(); action.onPress();
// Don't auto-close here if the action handles it, or check if we should
// Standard behavior is to close
onClose(); onClose();
} catch (error) { } catch (error) {
console.warn('[CustomAlert] Error in action handler:', error); console.warn('[CustomAlert] Error in action handler:', error);
// Still close the alert even if action fails
onClose(); onClose();
} }
}, [onClose]); }, [onClose]);
@ -91,7 +92,7 @@ export const CustomAlert = ({
<Animated.View <Animated.View
style={[ style={[
styles.overlay, styles.overlay,
{ backgroundColor: 'rgba(0,0,0,0.6)' }, { backgroundColor: 'rgba(0, 0, 0, 0.85)' },
overlayStyle overlayStyle
]} ]}
> >
@ -100,23 +101,22 @@ export const CustomAlert = ({
<Animated.View style={[ <Animated.View style={[
styles.alertContainer, styles.alertContainer,
alertStyle, alertStyle,
{
backgroundColor: themeColors.darkBackground,
borderColor: themeColors.primary,
}
]}> ]}>
{/* Title */} {/* Title */}
<Text style={[styles.title, { color: themeColors.highEmphasis }]}> <Text style={styles.title}>
{title} {title}
</Text> </Text>
{/* Message */} {/* Message */}
<Text style={[styles.message, { color: themeColors.mediumEmphasis }]}> <Text style={styles.message}>
{message} {message}
</Text> </Text>
{/* Actions */} {/* Actions */}
<View style={styles.actionsRow}> <View style={[
styles.actionsRow,
actions.length === 1 && { justifyContent: 'center' }
]}>
{actions.map((action, idx) => { {actions.map((action, idx) => {
const isPrimary = idx === actions.length - 1; const isPrimary = idx === actions.length - 1;
return ( return (
@ -125,9 +125,10 @@ export const CustomAlert = ({
style={[ style={[
styles.actionButton, styles.actionButton,
isPrimary isPrimary
? { ...styles.primaryButton, backgroundColor: themeColors.primary } ? { backgroundColor: themeColors.primary }
: styles.secondaryButton, : styles.secondaryButton,
action.style action.style,
actions.length === 1 && { minWidth: 120, maxWidth: '100%' }
]} ]}
onPress={() => handleActionPress(action)} onPress={() => handleActionPress(action)}
activeOpacity={0.7} activeOpacity={0.7}
@ -135,8 +136,8 @@ export const CustomAlert = ({
<Text style={[ <Text style={[
styles.actionText, styles.actionText,
isPrimary isPrimary
? { color: themeColors.white } ? { color: '#FFFFFF' }
: { color: themeColors.primary } : { color: '#FFFFFF' }
]}> ]}>
{action.label} {action.label}
</Text> </Text>
@ -157,6 +158,7 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 9999,
}, },
overlayPressable: { overlayPressable: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
@ -165,29 +167,32 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 24, paddingHorizontal: 20,
width: '100%',
}, },
alertContainer: { alertContainer: {
width: '100%', width: '100%',
maxWidth: 340, maxWidth: 400,
borderRadius: 24, backgroundColor: '#1E1E1E', // Solid opaque dark background
padding: 28, borderRadius: 16,
padding: 24,
borderWidth: 1, borderWidth: 1,
borderColor: '#007AFF', // iOS blue - will be overridden by theme borderColor: 'rgba(255, 255, 255, 0.1)',
overflow: 'hidden', // Ensure background fills entire card overflow: 'hidden',
...Platform.select({ ...Platform.select({
ios: { ios: {
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 8 }, shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3, shadowOpacity: 0.51,
shadowRadius: 24, shadowRadius: 13.16,
}, },
android: { android: {
elevation: 12, elevation: 20,
}, },
}), }),
}, },
title: { title: {
color: '#FFFFFF',
fontSize: 20, fontSize: 20,
fontWeight: '700', fontWeight: '700',
marginBottom: 8, marginBottom: 8,
@ -195,6 +200,7 @@ const styles = StyleSheet.create({
letterSpacing: 0.2, letterSpacing: 0.2,
}, },
message: { message: {
color: '#AAAAAA',
fontSize: 15, fontSize: 15,
marginBottom: 24, marginBottom: 24,
textAlign: 'center', textAlign: 'center',
@ -209,17 +215,16 @@ const styles = StyleSheet.create({
}, },
actionButton: { actionButton: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 11, paddingVertical: 12,
borderRadius: 12, borderRadius: 12,
minWidth: 80, minWidth: 80,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, flex: 1, // Distribute space
primaryButton: { maxWidth: 200, // But limit width
// Background color set dynamically via theme
}, },
secondaryButton: { secondaryButton: {
backgroundColor: 'rgba(255, 255, 255, 0.1)', backgroundColor: 'rgba(255, 255, 255, 0.08)',
}, },
actionText: { actionText: {
fontSize: 16, fontSize: 16,

View file

@ -78,17 +78,17 @@ const createStyles = (colors: any) => StyleSheet.create({
padding: 16, padding: 16,
}, },
sectionTitle: { sectionTitle: {
fontSize: 20, fontSize: 20,
fontWeight: '600', fontWeight: '600',
color: colors.white, color: colors.white,
marginBottom: 8, marginBottom: 8,
}, },
sectionHeader: { sectionHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 16, marginBottom: 16,
}, },
sectionDescription: { sectionDescription: {
fontSize: 14, fontSize: 14,
color: colors.mediumGray, color: colors.mediumGray,
@ -283,59 +283,59 @@ const createStyles = (colors: any) => StyleSheet.create({
marginTop: 8, marginTop: 8,
}, },
infoText: { infoText: {
fontSize: 14, fontSize: 14,
color: colors.mediumEmphasis, color: colors.mediumEmphasis,
lineHeight: 20, lineHeight: 20,
}, },
content: { content: {
flex: 1, flex: 1,
}, },
emptyState: { emptyState: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 32, paddingVertical: 32,
}, },
emptyStateTitle: { emptyStateTitle: {
fontSize: 18, fontSize: 18,
fontWeight: '600', fontWeight: '600',
color: colors.white, color: colors.white,
marginTop: 16, marginTop: 16,
marginBottom: 8, marginBottom: 8,
}, },
emptyStateDescription: { emptyStateDescription: {
fontSize: 14, fontSize: 14,
color: colors.mediumGray, color: colors.mediumGray,
textAlign: 'center', textAlign: 'center',
lineHeight: 20, lineHeight: 20,
}, },
scrapersList: { scrapersList: {
gap: 12, gap: 12,
}, },
scrapersContainer: { scrapersContainer: {
marginBottom: 24, marginBottom: 24,
}, },
inputContainer: { inputContainer: {
marginBottom: 16, marginBottom: 16,
}, },
lastSection: { lastSection: {
borderBottomWidth: 0, borderBottomWidth: 0,
}, },
disabledSection: { disabledSection: {
opacity: 0.5, opacity: 0.5,
}, },
disabledText: { disabledText: {
color: colors.elevation3, color: colors.elevation3,
}, },
disabledContainer: { disabledContainer: {
opacity: 0.5, opacity: 0.5,
}, },
disabledInput: { disabledInput: {
backgroundColor: colors.elevation1, backgroundColor: colors.elevation1,
opacity: 0.5, opacity: 0.5,
}, },
disabledButton: { disabledButton: {
opacity: 0.5, opacity: 0.5,
}, },
disabledImage: { disabledImage: {
opacity: 0.3, opacity: 0.3,
}, },
availableIndicator: { availableIndicator: {
@ -484,46 +484,60 @@ const createStyles = (colors: any) => StyleSheet.create({
}, },
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: 'rgba(0, 0, 0, 0.85)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
padding: 20,
}, },
modalContent: { modalContent: {
backgroundColor: colors.darkBackground, backgroundColor: '#1E1E1E', // Match CustomAlert
borderRadius: 16, borderRadius: 16,
padding: 20, padding: 24,
margin: 20, width: '100%',
maxHeight: '70%', maxWidth: 400,
width: screenWidth - 40,
borderWidth: 1, borderWidth: 1,
borderColor: colors.elevation3, borderColor: 'rgba(255, 255, 255, 0.1)',
alignSelf: 'center',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.51,
shadowRadius: 13.16,
},
android: {
elevation: 20,
},
}),
}, },
modalTitle: { modalTitle: {
fontSize: 18, fontSize: 20,
fontWeight: '600', fontWeight: '700',
color: colors.white, color: '#FFFFFF',
marginBottom: 8, marginBottom: 8,
textAlign: 'center',
}, },
modalText: { modalText: {
fontSize: 16, fontSize: 15,
color: colors.mediumGray, color: '#AAAAAA',
lineHeight: 24, lineHeight: 22,
marginBottom: 16, marginBottom: 16,
textAlign: 'center',
}, },
modalButton: { modalButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
paddingVertical: 14, paddingVertical: 12,
paddingHorizontal: 24, paddingHorizontal: 20,
borderRadius: 8, borderRadius: 12,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginTop: 16, marginTop: 16,
minHeight: 48, minHeight: 48,
}, },
modalButtonText: { modalButtonText: {
color: colors.white, color: '#FFFFFF',
fontSize: 16, fontSize: 16,
fontWeight: '500', fontWeight: '600',
}, },
// Compact modal styles // Compact modal styles
modalHeader: { modalHeader: {
@ -761,10 +775,10 @@ const CollapsibleSection: React.FC<{
<View style={styles.collapsibleSection}> <View style={styles.collapsibleSection}>
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}> <TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
<Text style={styles.collapsibleTitle}>{title}</Text> <Text style={styles.collapsibleTitle}>{title}</Text>
<Ionicons <Ionicons
name={isExpanded ? "chevron-up" : "chevron-down"} name={isExpanded ? "chevron-up" : "chevron-down"}
size={20} size={20}
color={colors.mediumGray} color={colors.mediumGray}
/> />
</TouchableOpacity> </TouchableOpacity>
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>} {isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
@ -803,7 +817,7 @@ const StatusBadge: React.FC<{
}; };
const config = getStatusConfig(); const config = getStatusConfig();
return ( return (
<View style={{ <View style={{
flexDirection: 'row', flexDirection: 'row',
@ -828,7 +842,7 @@ const PluginsScreen: React.FC = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const colors = currentTheme.colors; const colors = currentTheme.colors;
const styles = createStyles(colors); const styles = createStyles(colors);
// CustomAlert state // CustomAlert state
const [alertVisible, setAlertVisible] = useState(false); const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState(''); const [alertTitle, setAlertTitle] = useState('');
@ -842,7 +856,7 @@ const PluginsScreen: React.FC = () => {
) => { ) => {
setAlertTitle(title); setAlertTitle(title);
setAlertMessage(message); setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
setAlertVisible(true); setAlertVisible(true);
}; };
@ -856,14 +870,14 @@ const PluginsScreen: React.FC = () => {
const [showboxSavedToken, setShowboxSavedToken] = useState<string>(''); const [showboxSavedToken, setShowboxSavedToken] = useState<string>('');
const [showboxScraperId, setShowboxScraperId] = useState<string | null>(null); const [showboxScraperId, setShowboxScraperId] = useState<string | null>(null);
const [showboxTokenVisible, setShowboxTokenVisible] = useState<boolean>(false); const [showboxTokenVisible, setShowboxTokenVisible] = useState<boolean>(false);
// Multiple repositories state // Multiple repositories state
const [repositories, setRepositories] = useState<RepositoryInfo[]>([]); const [repositories, setRepositories] = useState<RepositoryInfo[]>([]);
const [currentRepositoryId, setCurrentRepositoryId] = useState<string>(''); const [currentRepositoryId, setCurrentRepositoryId] = useState<string>('');
const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false); const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false);
const [newRepositoryUrl, setNewRepositoryUrl] = useState(''); const [newRepositoryUrl, setNewRepositoryUrl] = useState('');
const [switchingRepository, setSwitchingRepository] = useState<string | null>(null); const [switchingRepository, setSwitchingRepository] = useState<string | null>(null);
// New UX state // New UX state
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all'); const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all');
@ -897,7 +911,7 @@ const PluginsScreen: React.FC = () => {
// Filter by search query // Filter by search query
if (searchQuery.trim()) { if (searchQuery.trim()) {
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
filtered = filtered.filter(scraper => filtered = filtered.filter(scraper =>
scraper.name.toLowerCase().includes(query) || scraper.name.toLowerCase().includes(query) ||
scraper.description.toLowerCase().includes(query) || scraper.description.toLowerCase().includes(query) ||
scraper.id.toLowerCase().includes(query) scraper.id.toLowerCase().includes(query)
@ -906,7 +920,7 @@ const PluginsScreen: React.FC = () => {
// Filter by type // Filter by type
if (selectedFilter !== 'all') { if (selectedFilter !== 'all') {
filtered = filtered.filter(scraper => filtered = filtered.filter(scraper =>
scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv') scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv')
); );
} }
@ -933,7 +947,7 @@ const PluginsScreen: React.FC = () => {
const handleBulkToggle = async (enabled: boolean) => { const handleBulkToggle = async (enabled: boolean) => {
try { try {
setIsRefreshing(true); setIsRefreshing(true);
const promises = filteredScrapers.map(scraper => const promises = filteredScrapers.map(scraper =>
pluginService.setScraperEnabled(scraper.id, enabled) pluginService.setScraperEnabled(scraper.id, enabled)
); );
await Promise.all(promises); await Promise.all(promises);
@ -994,14 +1008,14 @@ const PluginsScreen: React.FC = () => {
description: '', description: '',
enabled: true enabled: true
}); });
await loadRepositories(); await loadRepositories();
// Switch to the new repository and refresh it // Switch to the new repository and refresh it
await pluginService.setCurrentRepository(repoId); await pluginService.setCurrentRepository(repoId);
await loadRepositories(); await loadRepositories();
await loadScrapers(); await loadScrapers();
setNewRepositoryUrl(''); setNewRepositoryUrl('');
setShowAddRepositoryModal(false); setShowAddRepositoryModal(false);
openAlert('Success', 'Repository added and refreshed successfully'); openAlert('Success', 'Repository added and refreshed successfully');
@ -1034,9 +1048,9 @@ const PluginsScreen: React.FC = () => {
// Special handling for the last repository // Special handling for the last repository
const isLastRepository = repositories.length === 1; const isLastRepository = repositories.length === 1;
const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository'; const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository';
const alertMessage = isLastRepository const alertMessage = isLastRepository
? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no scrapers available until you add a new repository.` ? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no scrapers available until you add a new repository.`
: `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`; : `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`;
@ -1044,7 +1058,7 @@ const PluginsScreen: React.FC = () => {
alertTitle, alertTitle,
alertMessage, alertMessage,
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Remove', label: 'Remove',
onPress: async () => { onPress: async () => {
@ -1052,7 +1066,7 @@ const PluginsScreen: React.FC = () => {
await pluginService.removeRepository(repoId); await pluginService.removeRepository(repoId);
await loadRepositories(); await loadRepositories();
await loadScrapers(); await loadScrapers();
const successMessage = isLastRepository const successMessage = isLastRepository
? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.' ? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.'
: 'Repository removed successfully'; : 'Repository removed successfully';
openAlert('Success', successMessage); openAlert('Success', successMessage);
@ -1105,14 +1119,14 @@ const PluginsScreen: React.FC = () => {
try { try {
// First refresh repository names from manifests for existing repositories // First refresh repository names from manifests for existing repositories
await pluginService.refreshRepositoryNamesFromManifests(); await pluginService.refreshRepositoryNamesFromManifests();
const repos = await pluginService.getRepositories(); const repos = await pluginService.getRepositories();
setRepositories(repos); setRepositories(repos);
setHasRepository(repos.length > 0); setHasRepository(repos.length > 0);
const currentRepoId = pluginService.getCurrentRepositoryId(); const currentRepoId = pluginService.getCurrentRepositoryId();
setCurrentRepositoryId(currentRepoId); setCurrentRepositoryId(currentRepoId);
const currentRepo = repos.find(r => r.id === currentRepoId); const currentRepo = repos.find(r => r.id === currentRepoId);
if (currentRepo) { if (currentRepo) {
setRepositoryUrl(currentRepo.url); setRepositoryUrl(currentRepo.url);
@ -1144,7 +1158,7 @@ const PluginsScreen: React.FC = () => {
const url = repositoryUrl.trim(); const url = repositoryUrl.trim();
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
openAlert( openAlert(
'Invalid URL Format', 'Invalid URL Format',
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master' 'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master'
); );
return; return;
@ -1199,7 +1213,7 @@ const PluginsScreen: React.FC = () => {
// If enabling a scraper, ensure it's installed first // If enabling a scraper, ensure it's installed first
const installedScrapers = await pluginService.getInstalledScrapers(); const installedScrapers = await pluginService.getInstalledScrapers();
const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId); const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId);
if (!isInstalled) { if (!isInstalled) {
// Need to install the scraper first // Need to install the scraper first
setIsRefreshing(true); setIsRefreshing(true);
@ -1207,7 +1221,7 @@ const PluginsScreen: React.FC = () => {
setIsRefreshing(false); setIsRefreshing(false);
} }
} }
await pluginService.setScraperEnabled(scraperId, enabled); await pluginService.setScraperEnabled(scraperId, enabled);
await loadScrapers(); await loadScrapers();
} catch (error) { } catch (error) {
@ -1222,7 +1236,7 @@ const PluginsScreen: React.FC = () => {
'Clear All Scrapers', 'Clear All Scrapers',
'Are you sure you want to remove all installed scrapers? This action cannot be undone.', 'Are you sure you want to remove all installed scrapers? This action cannot be undone.',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Clear', label: 'Clear',
onPress: async () => { onPress: async () => {
@ -1245,7 +1259,7 @@ const PluginsScreen: React.FC = () => {
'Clear Repository Cache', '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.', 'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Clear Cache', label: 'Clear Cache',
onPress: async () => { onPress: async () => {
@ -1274,19 +1288,19 @@ const PluginsScreen: React.FC = () => {
const handleToggleLocalScrapers = async (enabled: boolean) => { const handleToggleLocalScrapers = async (enabled: boolean) => {
await updateSetting('enableLocalScrapers', enabled); await updateSetting('enableLocalScrapers', enabled);
// If enabling plugins, refresh repository and reload plugins // If enabling plugins, refresh repository and reload plugins
if (enabled) { if (enabled) {
try { try {
setIsRefreshing(true); setIsRefreshing(true);
logger.log('[PluginsScreen] Enabling plugins - refreshing repository...'); logger.log('[PluginsScreen] Enabling plugins - refreshing repository...');
// Refresh repository to ensure plugins are available // Refresh repository to ensure plugins are available
await pluginService.refreshRepository(); await pluginService.refreshRepository();
// Reload plugins to get the latest state // Reload plugins to get the latest state
await loadScrapers(); await loadScrapers();
logger.log('[PluginsScreen] Plugins enabled and repository refreshed'); logger.log('[PluginsScreen] Plugins enabled and repository refreshed');
} catch (error) { } catch (error) {
logger.error('[PluginsScreen] Failed to refresh repository when enabling plugins:', error); logger.error('[PluginsScreen] Failed to refresh repository when enabling plugins:', error);
@ -1304,7 +1318,7 @@ const PluginsScreen: React.FC = () => {
const handleToggleQualityExclusion = async (quality: string) => { const handleToggleQualityExclusion = async (quality: string) => {
const currentExcluded = settings.excludedQualities || []; const currentExcluded = settings.excludedQualities || [];
const isExcluded = currentExcluded.includes(quality); const isExcluded = currentExcluded.includes(quality);
let newExcluded: string[]; let newExcluded: string[];
if (isExcluded) { if (isExcluded) {
// Remove from excluded list // Remove from excluded list
@ -1313,14 +1327,14 @@ const PluginsScreen: React.FC = () => {
// Add to excluded list // Add to excluded list
newExcluded = [...currentExcluded, quality]; newExcluded = [...currentExcluded, quality];
} }
await updateSetting('excludedQualities', newExcluded); await updateSetting('excludedQualities', newExcluded);
}; };
const handleToggleLanguageExclusion = async (language: string) => { const handleToggleLanguageExclusion = async (language: string) => {
const currentExcluded = settings.excludedLanguages || []; const currentExcluded = settings.excludedLanguages || [];
const isExcluded = currentExcluded.includes(language); const isExcluded = currentExcluded.includes(language);
let newExcluded: string[]; let newExcluded: string[];
if (isExcluded) { if (isExcluded) {
// Remove from excluded list // Remove from excluded list
@ -1329,13 +1343,13 @@ const PluginsScreen: React.FC = () => {
// Add to excluded list // Add to excluded list
newExcluded = [...currentExcluded, language]; newExcluded = [...currentExcluded, language];
} }
await updateSetting('excludedLanguages', newExcluded); await updateSetting('excludedLanguages', newExcluded);
}; };
// Define available quality options // Define available quality options
const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS']; const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS'];
// Define available language options // Define available language options
const languageOptions = ['Original', 'English', 'Spanish', 'Latin', 'French', 'German', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Korean', 'Chinese', 'Arabic', 'Hindi', 'Turkish', 'Dutch', 'Polish']; const languageOptions = ['Original', 'English', 'Spanish', 'Latin', 'French', 'German', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Korean', 'Chinese', 'Arabic', 'Hindi', 'Turkish', 'Dutch', 'Polish'];
@ -1344,7 +1358,7 @@ const PluginsScreen: React.FC = () => {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
@ -1354,7 +1368,7 @@ const PluginsScreen: React.FC = () => {
<Ionicons name="arrow-back" size={24} color={colors.primary} /> <Ionicons name="arrow-back" size={24} color={colors.primary} />
<Text style={styles.backText}>Settings</Text> <Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Help Button */} {/* Help Button */}
<TouchableOpacity <TouchableOpacity
@ -1365,7 +1379,7 @@ const PluginsScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<Text style={styles.headerTitle}>Plugins</Text> <Text style={styles.headerTitle}>Plugins</Text>
<ScrollView <ScrollView
@ -1429,7 +1443,7 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.sectionDescription}> <Text style={styles.sectionDescription}>
Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers. Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers.
</Text> </Text>
{/* Current Repository */} {/* Current Repository */}
{currentRepositoryId && ( {currentRepositoryId && (
<View style={styles.currentRepoContainer}> <View style={styles.currentRepoContainer}>
@ -1438,7 +1452,7 @@ const PluginsScreen: React.FC = () => {
<Text style={[styles.currentRepoUrl, { fontSize: 12, opacity: 0.7, marginTop: 4 }]}>{repositoryUrl}</Text> <Text style={[styles.currentRepoUrl, { fontSize: 12, opacity: 0.7, marginTop: 4 }]}>{repositoryUrl}</Text>
</View> </View>
)} )}
{/* Repository List */} {/* Repository List */}
{repositories.length > 0 && ( {repositories.length > 0 && (
<View style={styles.repositoriesList}> <View style={styles.repositoriesList}>
@ -1467,8 +1481,8 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.repositoryUrl}>{repo.url}</Text> <Text style={styles.repositoryUrl}>{repo.url}</Text>
<Text style={styles.repositoryMeta}> <Text style={styles.repositoryMeta}>
{repo.scraperCount || 0} scrapers Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'} {repo.scraperCount || 0} scrapers Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
</Text> </Text>
</View> </View>
<View style={styles.repositoryActions}> <View style={styles.repositoryActions}>
{repo.id !== currentRepositoryId && ( {repo.id !== currentRepositoryId && (
<TouchableOpacity <TouchableOpacity
@ -1502,7 +1516,7 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.repositoryActionButtonText}>Remove</Text> <Text style={styles.repositoryActionButtonText}>Remove</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
))} ))}
</View> </View>
)} )}
@ -1541,9 +1555,9 @@ const PluginsScreen: React.FC = () => {
{searchQuery.length > 0 && ( {searchQuery.length > 0 && (
<TouchableOpacity onPress={() => setSearchQuery('')}> <TouchableOpacity onPress={() => setSearchQuery('')}>
<Ionicons name="close-circle" size={20} color={colors.mediumGray} /> <Ionicons name="close-circle" size={20} color={colors.mediumGray} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
{/* Filter Chips */} {/* Filter Chips */}
<View style={styles.filterContainer}> <View style={styles.filterContainer}>
@ -1561,7 +1575,7 @@ const PluginsScreen: React.FC = () => {
selectedFilter === filter && styles.filterChipTextSelected selectedFilter === filter && styles.filterChipTextSelected
]}> ]}>
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'} {filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
@ -1590,17 +1604,17 @@ const PluginsScreen: React.FC = () => {
{filteredScrapers.length === 0 ? ( {filteredScrapers.length === 0 ? (
<View style={styles.emptyStateContainer}> <View style={styles.emptyStateContainer}>
<Ionicons <Ionicons
name={searchQuery ? "search" : "download-outline"} name={searchQuery ? "search" : "download-outline"}
size={48} size={48}
color={colors.mediumGray} color={colors.mediumGray}
style={styles.emptyStateIcon} style={styles.emptyStateIcon}
/> />
<Text style={styles.emptyStateTitle}> <Text style={styles.emptyStateTitle}>
{searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'} {searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'}
</Text> </Text>
<Text style={styles.emptyStateDescription}> <Text style={styles.emptyStateDescription}>
{searchQuery {searchQuery
? `No scrapers match "${searchQuery}". Try a different search term.` ? `No scrapers match "${searchQuery}". Try a different search term.`
: 'Configure a repository above to view available scrapers.' : 'Configure a repository above to view available scrapers.'
} }
@ -1613,45 +1627,45 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.secondaryButtonText}>Clear Search</Text> <Text style={styles.secondaryButtonText}>Clear Search</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
) : ( ) : (
<View style={styles.scrapersContainer}> <View style={styles.scrapersContainer}>
{filteredScrapers.map((scraper) => ( {filteredScrapers.map((scraper) => (
<View key={scraper.id} style={styles.scraperCard}> <View key={scraper.id} style={styles.scraperCard}>
<View style={styles.scraperCardHeader}> <View style={styles.scraperCardHeader}>
{scraper.logo ? ( {scraper.logo ? (
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? ( (scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
<Image <Image
source={{ uri: scraper.logo }} source={{ uri: scraper.logo }}
style={styles.scraperLogo} style={styles.scraperLogo}
resizeMode="contain" resizeMode="contain"
/> />
) : ( ) : (
<FastImage <FastImage
source={{ uri: scraper.logo }} source={{ uri: scraper.logo }}
style={styles.scraperLogo} style={styles.scraperLogo}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
) )
) : ( ) : (
<View style={styles.scraperLogo} /> <View style={styles.scraperLogo} />
)} )}
<View style={styles.scraperCardInfo}> <View style={styles.scraperCardInfo}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
<Text style={styles.scraperName}>{scraper.name}</Text> <Text style={styles.scraperName}>{scraper.name}</Text>
<StatusBadge status={getScraperStatus(scraper)} colors={colors} /> <StatusBadge status={getScraperStatus(scraper)} colors={colors} />
</View>
<Text style={styles.scraperDescription}>{scraper.description}</Text>
</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 || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
/>
</View> </View>
<Text style={styles.scraperDescription}>{scraper.description}</Text>
</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 || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
/>
</View>
<View style={styles.scraperCardMeta}> <View style={styles.scraperCardMeta}>
<View style={styles.scraperCardMetaItem}> <View style={styles.scraperCardMetaItem}>
<Ionicons name="information-circle" size={12} color={colors.mediumGray} /> <Ionicons name="information-circle" size={12} color={colors.mediumGray} />
@ -1682,62 +1696,62 @@ const PluginsScreen: React.FC = () => {
</View> </View>
{/* ShowBox Settings - only visible when ShowBox scraper is available */} {/* ShowBox Settings - only visible when ShowBox scraper is available */}
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && ( {showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}> <View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text> <Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}> <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
<TextInput <TextInput
style={[styles.textInput, { flex: 1, marginBottom: 0 }]} style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
value={showboxUiToken} value={showboxUiToken}
onChangeText={setShowboxUiToken} onChangeText={setShowboxUiToken}
placeholder="Paste your ShowBox UI token" placeholder="Paste your ShowBox UI token"
placeholderTextColor={colors.mediumGray} placeholderTextColor={colors.mediumGray}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible} secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
multiline={false} multiline={false}
numberOfLines={1} numberOfLines={1}
/> />
{showboxSavedToken.length > 0 && ( {showboxSavedToken.length > 0 && (
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}> <TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} /> <Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
{showboxUiToken !== showboxSavedToken && ( {showboxUiToken !== showboxSavedToken && (
<TouchableOpacity <TouchableOpacity
style={[styles.button, styles.primaryButton]} style={[styles.button, styles.primaryButton]}
onPress={async () => { onPress={async () => {
if (showboxScraperId) { if (showboxScraperId) {
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken }); await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
} }
setShowboxSavedToken(showboxUiToken); setShowboxSavedToken(showboxUiToken);
openAlert('Saved', 'ShowBox settings updated'); openAlert('Saved', 'ShowBox settings updated');
}} }}
> >
<Text style={styles.buttonText}>Save</Text> <Text style={styles.buttonText}>Save</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
<TouchableOpacity <TouchableOpacity
style={[styles.button, styles.secondaryButton]} style={[styles.button, styles.secondaryButton]}
onPress={async () => { onPress={async () => {
setShowboxUiToken(''); setShowboxUiToken('');
setShowboxSavedToken(''); setShowboxSavedToken('');
if (showboxScraperId) { if (showboxScraperId) {
await pluginService.setScraperSettings(showboxScraperId, {}); await pluginService.setScraperSettings(showboxScraperId, {});
} }
}} }}
> >
<Text style={styles.secondaryButtonText}>Clear</Text> <Text style={styles.secondaryButtonText}>Clear</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
)} )}
</View> </View>
))} ))}
</View> </View>
)} )}
</CollapsibleSection> </CollapsibleSection>
{/* Additional Settings */} {/* Additional Settings */}
@ -1763,7 +1777,7 @@ const PluginsScreen: React.FC = () => {
disabled={!settings.enableLocalScrapers} disabled={!settings.enableLocalScrapers}
/> />
</View> </View>
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingInfo}> <View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Group Plugin Streams</Text> <Text style={styles.settingTitle}>Group Plugin Streams</Text>
@ -1772,20 +1786,20 @@ const PluginsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<Switch <Switch
value={settings.streamDisplayMode === 'grouped'} value={settings.streamDisplayMode === 'grouped'}
onValueChange={(value) => { onValueChange={(value) => {
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate'); updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
// Auto-disable quality sorting when grouping is disabled // Auto-disable quality sorting when grouping is disabled
if (!value && settings.streamSortMode === 'quality-then-scraper') { if (!value && settings.streamSortMode === 'quality-then-scraper') {
updateSetting('streamSortMode', 'scraper-then-quality'); updateSetting('streamSortMode', 'scraper-then-quality');
} }
}} }}
trackColor={{ false: colors.elevation3, true: colors.primary }} trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'} thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers} disabled={!settings.enableLocalScrapers}
/> />
</View> </View>
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingInfo}> <View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Sort by Quality First</Text> <Text style={styles.settingTitle}>Sort by Quality First</Text>
@ -1794,14 +1808,14 @@ const PluginsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<Switch <Switch
value={settings.streamSortMode === 'quality-then-scraper'} value={settings.streamSortMode === 'quality-then-scraper'}
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')} onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
trackColor={{ false: colors.elevation3, true: colors.primary }} trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'} thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'} disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
/> />
</View> </View>
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingInfo}> <View style={styles.settingInfo}>
<Text style={styles.settingTitle}>Show Scraper Logos</Text> <Text style={styles.settingTitle}>Show Scraper Logos</Text>
@ -1810,12 +1824,12 @@ const PluginsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<Switch <Switch
value={settings.showScraperLogos && settings.enableLocalScrapers} value={settings.showScraperLogos && settings.enableLocalScrapers}
onValueChange={(value) => updateSetting('showScraperLogos', value)} onValueChange={(value) => updateSetting('showScraperLogos', value)}
trackColor={{ false: colors.elevation3, true: colors.primary }} trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers} disabled={!settings.enableLocalScrapers}
/> />
</View> </View>
</CollapsibleSection> </CollapsibleSection>
@ -1830,7 +1844,7 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.sectionDescription}> <Text style={styles.sectionDescription}>
Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results. Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
</Text> </Text>
<View style={styles.qualityChipsContainer}> <View style={styles.qualityChipsContainer}>
{qualityOptions.map((quality) => { {qualityOptions.map((quality) => {
const isExcluded = (settings.excludedQualities || []).includes(quality); const isExcluded = (settings.excludedQualities || []).includes(quality);
@ -1856,7 +1870,7 @@ const PluginsScreen: React.FC = () => {
); );
})} })}
</View> </View>
{(settings.excludedQualities || []).length > 0 && ( {(settings.excludedQualities || []).length > 0 && (
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}> <Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
Excluded qualities: {(settings.excludedQualities || []).join(', ')} Excluded qualities: {(settings.excludedQualities || []).join(', ')}
@ -1875,11 +1889,11 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.sectionDescription}> <Text style={styles.sectionDescription}>
Exclude specific languages from search results. Tap on a language to exclude it from plugin results. Exclude specific languages from search results. Tap on a language to exclude it from plugin results.
</Text> </Text>
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}> <Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
<Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers. <Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers.
</Text> </Text>
<View style={styles.qualityChipsContainer}> <View style={styles.qualityChipsContainer}>
{languageOptions.map((language) => { {languageOptions.map((language) => {
const isExcluded = (settings.excludedLanguages || []).includes(language); const isExcluded = (settings.excludedLanguages || []).includes(language);
@ -1905,7 +1919,7 @@ const PluginsScreen: React.FC = () => {
); );
})} })}
</View> </View>
{(settings.excludedLanguages || []).length > 0 && ( {(settings.excludedLanguages || []).length > 0 && (
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}> <Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
Excluded languages: {(settings.excludedLanguages || []).join(', ')} Excluded languages: {(settings.excludedLanguages || []).join(', ')}
@ -1988,36 +2002,36 @@ const PluginsScreen: React.FC = () => {
/> />
{/* Format Hint */} {/* Format Hint */}
<Text style={styles.formatHint}> <Text style={styles.formatHint}>
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
</Text> </Text>
{/* Action Buttons */} {/* Action Buttons */}
<View style={styles.compactActions}> <View style={styles.compactActions}>
<TouchableOpacity <TouchableOpacity
style={[styles.compactButton, styles.cancelButton]} style={[styles.compactButton, styles.cancelButton]}
onPress={() => { onPress={() => {
setShowAddRepositoryModal(false); setShowAddRepositoryModal(false);
setNewRepositoryUrl(''); setNewRepositoryUrl('');
}} }}
> >
<Text style={styles.cancelButtonText}>Cancel</Text> <Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]} style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
onPress={handleAddRepository} onPress={handleAddRepository}
disabled={!newRepositoryUrl.trim() || isLoading} disabled={!newRepositoryUrl.trim() || isLoading}
> >
{isLoading ? ( {isLoading ? (
<ActivityIndicator size="small" color={colors.white} /> <ActivityIndicator size="small" color={colors.white} />
) : ( ) : (
<Text style={styles.addButtonText}>Add</Text> <Text style={styles.addButtonText}>Add</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
</View> </View>
</View> </View>
</Modal> </Modal>