From c9ea142dfb4419b9807e8c16840bfa4c795c7a8c Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 6 Sep 2025 01:39:07 +0530 Subject: [PATCH] ui changes --- local-scrapers-repo | 2 +- src/screens/PluginsScreen.tsx | 1094 ++++++++++++++++++++------------- 2 files changed, 676 insertions(+), 420 deletions(-) diff --git a/local-scrapers-repo b/local-scrapers-repo index 62c4288..6fda05e 160000 --- a/local-scrapers-repo +++ b/local-scrapers-repo @@ -1 +1 @@ -Subproject commit 62c4288d6a0d1046a3959000400f2213aa4889a9 +Subproject commit 6fda05ee03e39bd6bd1ee9d15be00cb33b491574 diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index ea0598c..bb35e48 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, @@ -13,6 +13,9 @@ import { Platform, Image, ActivityIndicator, + Modal, + Dimensions, + Animated, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; @@ -22,6 +25,8 @@ import { localScraperService, ScraperInfo } from '../services/localScraperServic 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 @@ -355,14 +360,298 @@ const createStyles = (colors: any) => StyleSheet.create({ 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: 8, + }, + bulkActionButton: { + flex: 1, + paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 8, + alignItems: 'center', + }, + bulkActionButtonEnabled: { + backgroundColor: '#34C759', + }, + bulkActionButtonDisabled: { + backgroundColor: colors.elevation2, + borderWidth: 1, + borderColor: colors.elevation3, + }, + bulkActionButtonText: { + color: colors.white, + fontSize: 14, + fontWeight: '600', + }, + helpButton: { + position: 'absolute', + top: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16, + right: 16, + backgroundColor: colors.elevation2, + borderRadius: 20, + padding: 8, + }, + 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: 12, + paddingHorizontal: 24, + borderRadius: 8, + alignItems: 'center', + marginTop: 16, + }, + modalButtonText: { + color: colors.white, + fontSize: 16, + fontWeight: '600', + }, + 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: 10, + paddingHorizontal: 16, + borderRadius: 8, + alignItems: 'center', + }, + quickSetupButtonText: { + color: colors.white, + fontSize: 14, + fontWeight: '600', + }, + 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, + }, }); +// 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); @@ -370,6 +659,19 @@ const PluginsScreen: React.FC = () => { const [hasRepository, setHasRepository] = useState(false); const [showboxCookie, setShowboxCookie] = useState(''); const [showboxRegion, setShowboxRegion] = useState(''); + + // 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' }, @@ -384,6 +686,62 @@ const PluginsScreen: React.FC = () => { { 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); + } + }; + useEffect(() => { loadScrapers(); checkRepository(); @@ -586,6 +944,8 @@ const PluginsScreen: React.FC = () => { barStyle={Platform.OS === 'ios' ? 'light-content' : 'light-content'} backgroundColor={colors.background} /> + + {/* Header */} { Settings + + {/* Help Button */} + setShowHelpModal(true)} + > + + Plugins @@ -604,8 +972,33 @@ const PluginsScreen: React.FC = () => { } > - {/* Enable Local Scrapers - Top Priority */} - + {/* 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 @@ -620,30 +1013,25 @@ const PluginsScreen: React.FC = () => { thumbColor={settings.enableLocalScrapers ? colors.white : '#f4f3f4'} /> - + - {/* Repository Configuration - Moved up for better UX */} - - - Repository Configuration - {hasRepository && settings.enableLocalScrapers && ( - - Clear Cache - - )} - - + {/* Repository Configuration */} + toggleSection('repository')} + colors={colors} + styles={styles} + > + Enter the URL of a Nuvio scraper repository to download and install scrapers. {hasRepository && repositoryUrl && ( - - Current Repository: - {localScraperService.getRepositoryName()} - {repositoryUrl} + + Current Repository: + {localScraperService.getRepositoryName()} + {repositoryUrl} )} @@ -660,7 +1048,7 @@ const PluginsScreen: React.FC = () => { editable={settings.enableLocalScrapers} /> - 💡 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 { )} - + {/* Available Scrapers */} - - - Available Scrapers - {installedScrapers.length > 0 && settings.enableLocalScrapers && ( - - Clear All - - )} - - - Scrapers available in the repository. Only enabled scrapers that are also installed will be used for streaming. - + toggleSection('scrapers')} + colors={colors} + styles={styles} + > + {installedScrapers.length > 0 && ( + <> + {/* Search and Filter */} + + + + {searchQuery.length > 0 && ( + setSearchQuery('')}> + + + )} + - {installedScrapers.length === 0 ? ( - - - No Scrapers Available - - Configure a repository above to view available scrapers. - - - ) : ( - - {installedScrapers.map((scraper) => { - return ( - - - {scraper.logo ? ( - - ) : ( - - )} - - - {scraper.name} - {scraper.manifestEnabled === false ? ( - - Disabled - - ) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? ( - - Platform Disabled - - ) : !scraper.enabled && ( - - Available - - )} - - {scraper.description} - - v{scraper.version} - • - - {scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'} - - {scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && ( - <> - • - - {scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} - - - )} - - - handleToggleScraper(scraper.id, enabled)} - trackColor={{ false: colors.elevation3, true: colors.primary }} - thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} - disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))} - style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))) ? 0.5 : 1 }} - /> + {/* 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.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 - - - - )} - - ); - })} - - )} - + {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(', ')} + + + )} + - {/* Additional Scraper Settings */} - - Additional Settings + {/* 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 - + Enable URL Validation + Validate streaming URLs before returning them (may slow down results but improves reliability) @@ -866,8 +1323,8 @@ const PluginsScreen: React.FC = () => { - Group Plugin Streams - + Group Plugin Streams + When enabled, all plugin streams are grouped under "{localScraperService.getRepositoryName()}". When disabled, each plugin shows as a separate provider. @@ -888,8 +1345,8 @@ const PluginsScreen: React.FC = () => { - Sort by Quality First - + 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. @@ -904,8 +1361,8 @@ const PluginsScreen: React.FC = () => { - Show Scraper Logos - + Show Scraper Logos + Display scraper logos next to streaming links on the streams screen. @@ -917,12 +1374,17 @@ const PluginsScreen: React.FC = () => { disabled={!settings.enableLocalScrapers} /> - + {/* Quality Filtering */} - - 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. @@ -954,10 +1416,10 @@ const PluginsScreen: React.FC = () => { {(settings.excludedQualities || []).length > 0 && ( - 💡 Excluded qualities: {(settings.excludedQualities || []).join(', ')} + Excluded qualities: {(settings.excludedQualities || []).join(', ')} )} - + {/* About */} @@ -968,246 +1430,40 @@ const PluginsScreen: React.FC = () => { + + {/* Help Modal */} + setShowHelpModal(false)} + > + + + Getting Started with Plugins + + 1. Enable Local Scrapers - Turn on the main switch to allow plugins + + + 2. Set Repository URL - Enter 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! + + + + ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000000', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 12, - borderBottomWidth: 1, - borderBottomColor: '#333', - }, - backButton: { - marginRight: 16, - }, - headerTitle: { - fontSize: 20, - fontWeight: '600', - color: '#ffffff', - }, - content: { - flex: 1, - }, - section: { - padding: 16, - borderBottomWidth: 1, - borderBottomColor: '#333', - }, - lastSection: { - borderBottomWidth: 0, - }, - sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - marginHorizontal: -16, - paddingHorizontal: 16, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '600', - color: '#ffffff', - marginBottom: 8, - }, - sectionDescription: { - fontSize: 14, - color: '#999', - marginBottom: 16, - lineHeight: 20, - }, - settingRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - settingInfo: { - flex: 1, - marginRight: 16, - }, - settingTitle: { - fontSize: 16, - fontWeight: '500', - color: '#ffffff', - marginBottom: 4, - }, - settingDescription: { - fontSize: 14, - color: '#999', - lineHeight: 18, - }, - inputContainer: { - marginBottom: 16, - }, - textInput: { - backgroundColor: '#1a1a1a', - borderRadius: 8, - padding: 12, - fontSize: 16, - color: '#ffffff', - borderWidth: 1, - borderColor: '#333', - }, - buttonRow: { - flexDirection: 'row', - gap: 12, - }, - button: { - flex: 1, - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - minHeight: 44, - }, - primaryButton: { - backgroundColor: '#007AFF', - }, - secondaryButton: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: '#007AFF', - }, - buttonText: { - color: '#ffffff', - fontSize: 16, - fontWeight: '600', - }, - secondaryButtonText: { - color: '#007AFF', - fontSize: 16, - fontWeight: '600', - }, - clearButton: { - paddingVertical: 6, - paddingHorizontal: 12, - borderRadius: 6, - backgroundColor: '#ff3b30', - marginLeft: 0, - }, - clearButtonText: { - color: '#ffffff', - fontSize: 14, - fontWeight: '500', - }, - scrapersList: { - gap: 12, - }, - scraperItem: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#1a1a1a', - borderRadius: 8, - padding: 16, - borderWidth: 1, - borderColor: '#333', - }, - scraperLogo: { - width: 40, - height: 40, - marginRight: 12, - borderRadius: 8, - }, - scraperInfo: { - flex: 1, - marginRight: 16, - }, - scraperName: { - fontSize: 16, - fontWeight: '600', - color: '#ffffff', - marginBottom: 4, - }, - scraperDescription: { - fontSize: 14, - color: '#999', - marginBottom: 8, - lineHeight: 18, - }, - scraperMeta: { - flexDirection: 'row', - gap: 12, - }, - scraperVersion: { - fontSize: 12, - color: '#007AFF', - fontWeight: '500', - }, - scraperTypes: { - fontSize: 12, - color: '#666', - textTransform: 'uppercase', - }, - emptyState: { - alignItems: 'center', - paddingVertical: 32, - }, - emptyStateTitle: { - fontSize: 18, - fontWeight: '600', - color: '#ffffff', - marginTop: 16, - marginBottom: 8, - }, - emptyStateDescription: { - fontSize: 14, - color: '#999', - textAlign: 'center', - lineHeight: 20, - }, - infoText: { - fontSize: 14, - color: '#999', - lineHeight: 20, - marginBottom: 12, - }, - currentRepoContainer: { - backgroundColor: '#1a1a1a', - borderRadius: 8, - padding: 12, - marginBottom: 16, - borderWidth: 1, - borderColor: '#333', - }, - currentRepoLabel: { - fontSize: 14, - fontWeight: '500', - color: '#007AFF', - marginBottom: 4, - }, - currentRepoUrl: { - fontSize: 14, - color: '#ffffff', - fontFamily: 'monospace', - lineHeight: 18, - }, - urlHint: { - fontSize: 12, - color: '#666', - marginTop: 8, - lineHeight: 16, - }, - defaultRepoButton: { - backgroundColor: '#333', - borderRadius: 6, - paddingVertical: 8, - paddingHorizontal: 12, - marginTop: 8, - alignItems: 'center', - }, - defaultRepoButtonText: { - color: '#007AFF', - fontSize: 14, - fontWeight: '500', - }, -}); export default PluginsScreen; \ No newline at end of file