From 59cb9026589de9eb79342d03f654f2a15a9f5d5d Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 16 Dec 2025 15:24:32 +0530 Subject: [PATCH] revamped alert UI --- .gitignore | 3 +- src/components/CustomAlert.tsx | 65 ++-- src/screens/PluginsScreen.tsx | 568 +++++++++++++++++---------------- 3 files changed, 328 insertions(+), 308 deletions(-) diff --git a/.gitignore b/.gitignore index 3736957..fd89a97 100644 --- a/.gitignore +++ b/.gitignore @@ -85,4 +85,5 @@ node_modules expofs.md ios/sentry.properties android/sentry.properties -Stremio addons refer \ No newline at end of file +Stremio addons refer +trakt-docs \ No newline at end of file diff --git a/src/components/CustomAlert.tsx b/src/components/CustomAlert.tsx index 4a0588f..14fb993 100644 --- a/src/components/CustomAlert.tsx +++ b/src/components/CustomAlert.tsx @@ -15,7 +15,7 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import { useTheme } from '../contexts/ThemeContext'; -import { Portal, Dialog, Button } from 'react-native-paper'; +import { Portal } from 'react-native-paper'; interface CustomAlertProps { visible: boolean; @@ -40,8 +40,8 @@ export const CustomAlert = ({ }: CustomAlertProps) => { const opacity = useSharedValue(0); const scale = useSharedValue(0.95); - const isDarkMode = useColorScheme() === 'dark'; const { currentTheme } = useTheme(); + // Using hardcoded dark theme values to match SeriesContent modal const themeColors = currentTheme.colors; useEffect(() => { @@ -68,10 +68,11 @@ export const CustomAlert = ({ const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => { try { action.onPress(); + // Don't auto-close here if the action handles it, or check if we should + // Standard behavior is to close onClose(); } catch (error) { console.warn('[CustomAlert] Error in action handler:', error); - // Still close the alert even if action fails onClose(); } }, [onClose]); @@ -91,7 +92,7 @@ export const CustomAlert = ({ @@ -100,23 +101,22 @@ export const CustomAlert = ({ {/* Title */} - + {title} {/* Message */} - + {message} {/* Actions */} - + {actions.map((action, idx) => { const isPrimary = idx === actions.length - 1; return ( @@ -125,9 +125,10 @@ export const CustomAlert = ({ style={[ styles.actionButton, isPrimary - ? { ...styles.primaryButton, backgroundColor: themeColors.primary } + ? { backgroundColor: themeColors.primary } : styles.secondaryButton, - action.style + action.style, + actions.length === 1 && { minWidth: 120, maxWidth: '100%' } ]} onPress={() => handleActionPress(action)} activeOpacity={0.7} @@ -135,8 +136,8 @@ export const CustomAlert = ({ {action.label} @@ -157,6 +158,7 @@ const styles = StyleSheet.create({ ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', + zIndex: 9999, }, overlayPressable: { ...StyleSheet.absoluteFillObject, @@ -165,29 +167,32 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', - paddingHorizontal: 24, + paddingHorizontal: 20, + width: '100%', }, alertContainer: { width: '100%', - maxWidth: 340, - borderRadius: 24, - padding: 28, + maxWidth: 400, + backgroundColor: '#1E1E1E', // Solid opaque dark background + borderRadius: 16, + padding: 24, borderWidth: 1, - borderColor: '#007AFF', // iOS blue - will be overridden by theme - overflow: 'hidden', // Ensure background fills entire card + borderColor: 'rgba(255, 255, 255, 0.1)', + overflow: 'hidden', ...Platform.select({ ios: { shadowColor: '#000', - shadowOffset: { width: 0, height: 8 }, - shadowOpacity: 0.3, - shadowRadius: 24, + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.51, + shadowRadius: 13.16, }, android: { - elevation: 12, + elevation: 20, }, }), }, title: { + color: '#FFFFFF', fontSize: 20, fontWeight: '700', marginBottom: 8, @@ -195,6 +200,7 @@ const styles = StyleSheet.create({ letterSpacing: 0.2, }, message: { + color: '#AAAAAA', fontSize: 15, marginBottom: 24, textAlign: 'center', @@ -209,17 +215,16 @@ const styles = StyleSheet.create({ }, actionButton: { paddingHorizontal: 20, - paddingVertical: 11, + paddingVertical: 12, borderRadius: 12, minWidth: 80, alignItems: 'center', justifyContent: 'center', - }, - primaryButton: { - // Background color set dynamically via theme + flex: 1, // Distribute space + maxWidth: 200, // But limit width }, secondaryButton: { - backgroundColor: 'rgba(255, 255, 255, 0.1)', + backgroundColor: 'rgba(255, 255, 255, 0.08)', }, actionText: { fontSize: 16, diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 9a08a26..f7e6cd1 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -78,17 +78,17 @@ const createStyles = (colors: any) => StyleSheet.create({ padding: 16, }, sectionTitle: { - fontSize: 20, - fontWeight: '600', - color: colors.white, - marginBottom: 8, - }, + fontSize: 20, + fontWeight: '600', + color: colors.white, + marginBottom: 8, + }, sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 16, - }, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 16, + }, sectionDescription: { fontSize: 14, color: colors.mediumGray, @@ -283,59 +283,59 @@ const createStyles = (colors: any) => StyleSheet.create({ 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: { + 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: { @@ -484,46 +484,60 @@ const createStyles = (colors: any) => StyleSheet.create({ }, modalOverlay: { flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', + backgroundColor: 'rgba(0, 0, 0, 0.85)', justifyContent: 'center', alignItems: 'center', + padding: 20, }, modalContent: { - backgroundColor: colors.darkBackground, + backgroundColor: '#1E1E1E', // Match CustomAlert borderRadius: 16, - padding: 20, - margin: 20, - maxHeight: '70%', - width: screenWidth - 40, + padding: 24, + width: '100%', + maxWidth: 400, 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: { - fontSize: 18, - fontWeight: '600', - color: colors.white, + fontSize: 20, + fontWeight: '700', + color: '#FFFFFF', marginBottom: 8, + textAlign: 'center', }, modalText: { - fontSize: 16, - color: colors.mediumGray, - lineHeight: 24, + fontSize: 15, + color: '#AAAAAA', + lineHeight: 22, marginBottom: 16, + textAlign: 'center', }, modalButton: { backgroundColor: colors.primary, - paddingVertical: 14, - paddingHorizontal: 24, - borderRadius: 8, + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 12, alignItems: 'center', justifyContent: 'center', marginTop: 16, minHeight: 48, }, modalButtonText: { - color: colors.white, + color: '#FFFFFF', fontSize: 16, - fontWeight: '500', + fontWeight: '600', }, // Compact modal styles modalHeader: { @@ -761,10 +775,10 @@ const CollapsibleSection: React.FC<{ {title} - {isExpanded && {children}} @@ -803,7 +817,7 @@ const StatusBadge: React.FC<{ }; const config = getStatusConfig(); - + return ( { const { currentTheme } = useTheme(); const colors = currentTheme.colors; const styles = createStyles(colors); - + // CustomAlert state const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); @@ -842,7 +856,7 @@ const PluginsScreen: React.FC = () => { ) => { setAlertTitle(title); setAlertMessage(message); - setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); setAlertVisible(true); }; @@ -856,14 +870,14 @@ const PluginsScreen: React.FC = () => { const [showboxSavedToken, setShowboxSavedToken] = useState(''); const [showboxScraperId, setShowboxScraperId] = useState(null); const [showboxTokenVisible, setShowboxTokenVisible] = useState(false); - + // Multiple repositories state const [repositories, setRepositories] = useState([]); const [currentRepositoryId, setCurrentRepositoryId] = useState(''); const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false); const [newRepositoryUrl, setNewRepositoryUrl] = useState(''); const [switchingRepository, setSwitchingRepository] = useState(null); - + // New UX state const [searchQuery, setSearchQuery] = useState(''); const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all'); @@ -897,7 +911,7 @@ const PluginsScreen: React.FC = () => { // Filter by search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); - filtered = filtered.filter(scraper => + filtered = filtered.filter(scraper => scraper.name.toLowerCase().includes(query) || scraper.description.toLowerCase().includes(query) || scraper.id.toLowerCase().includes(query) @@ -906,7 +920,7 @@ const PluginsScreen: React.FC = () => { // Filter by type if (selectedFilter !== 'all') { - filtered = filtered.filter(scraper => + filtered = filtered.filter(scraper => scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv') ); } @@ -933,7 +947,7 @@ const PluginsScreen: React.FC = () => { const handleBulkToggle = async (enabled: boolean) => { try { setIsRefreshing(true); - const promises = filteredScrapers.map(scraper => + const promises = filteredScrapers.map(scraper => pluginService.setScraperEnabled(scraper.id, enabled) ); await Promise.all(promises); @@ -994,14 +1008,14 @@ const PluginsScreen: React.FC = () => { description: '', enabled: true }); - + await loadRepositories(); - + // Switch to the new repository and refresh it await pluginService.setCurrentRepository(repoId); await loadRepositories(); await loadScrapers(); - + setNewRepositoryUrl(''); setShowAddRepositoryModal(false); openAlert('Success', 'Repository added and refreshed successfully'); @@ -1034,9 +1048,9 @@ const PluginsScreen: React.FC = () => { // Special handling for the last repository const isLastRepository = repositories.length === 1; - + 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 will also remove all scrapers from this repository.`; @@ -1044,7 +1058,7 @@ const PluginsScreen: React.FC = () => { alertTitle, alertMessage, [ - { label: 'Cancel', onPress: () => {} }, + { label: 'Cancel', onPress: () => { } }, { label: 'Remove', onPress: async () => { @@ -1052,7 +1066,7 @@ const PluginsScreen: React.FC = () => { await pluginService.removeRepository(repoId); await loadRepositories(); 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'; openAlert('Success', successMessage); @@ -1105,14 +1119,14 @@ const PluginsScreen: React.FC = () => { try { // First refresh repository names from manifests for existing repositories await pluginService.refreshRepositoryNamesFromManifests(); - + const repos = await pluginService.getRepositories(); setRepositories(repos); setHasRepository(repos.length > 0); - + const currentRepoId = pluginService.getCurrentRepositoryId(); setCurrentRepositoryId(currentRepoId); - + const currentRepo = repos.find(r => r.id === currentRepoId); if (currentRepo) { setRepositoryUrl(currentRepo.url); @@ -1144,7 +1158,7 @@ const PluginsScreen: React.FC = () => { const url = repositoryUrl.trim(); if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { 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' ); return; @@ -1199,7 +1213,7 @@ const PluginsScreen: React.FC = () => { // If enabling a scraper, ensure it's installed first const installedScrapers = await pluginService.getInstalledScrapers(); const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId); - + if (!isInstalled) { // Need to install the scraper first setIsRefreshing(true); @@ -1207,7 +1221,7 @@ const PluginsScreen: React.FC = () => { setIsRefreshing(false); } } - + await pluginService.setScraperEnabled(scraperId, enabled); await loadScrapers(); } catch (error) { @@ -1222,7 +1236,7 @@ const PluginsScreen: React.FC = () => { 'Clear All Scrapers', 'Are you sure you want to remove all installed scrapers? This action cannot be undone.', [ - { label: 'Cancel', onPress: () => {} }, + { label: 'Cancel', onPress: () => { } }, { label: 'Clear', onPress: async () => { @@ -1245,7 +1259,7 @@ const PluginsScreen: React.FC = () => { '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.', [ - { label: 'Cancel', onPress: () => {} }, + { label: 'Cancel', onPress: () => { } }, { label: 'Clear Cache', onPress: async () => { @@ -1274,19 +1288,19 @@ const PluginsScreen: React.FC = () => { const handleToggleLocalScrapers = async (enabled: boolean) => { await updateSetting('enableLocalScrapers', enabled); - + // If enabling plugins, refresh repository and reload plugins if (enabled) { try { setIsRefreshing(true); logger.log('[PluginsScreen] Enabling plugins - refreshing repository...'); - + // Refresh repository to ensure plugins are available await pluginService.refreshRepository(); - + // Reload plugins to get the latest state await loadScrapers(); - + logger.log('[PluginsScreen] Plugins enabled and repository refreshed'); } catch (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 currentExcluded = settings.excludedQualities || []; const isExcluded = currentExcluded.includes(quality); - + let newExcluded: string[]; if (isExcluded) { // Remove from excluded list @@ -1313,14 +1327,14 @@ const PluginsScreen: React.FC = () => { // Add to excluded list newExcluded = [...currentExcluded, quality]; } - + await updateSetting('excludedQualities', newExcluded); }; const handleToggleLanguageExclusion = async (language: string) => { const currentExcluded = settings.excludedLanguages || []; const isExcluded = currentExcluded.includes(language); - + let newExcluded: string[]; if (isExcluded) { // Remove from excluded list @@ -1329,13 +1343,13 @@ const PluginsScreen: React.FC = () => { // Add to excluded list newExcluded = [...currentExcluded, language]; } - + await updateSetting('excludedLanguages', newExcluded); }; // Define available quality options const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS']; - + // Define available language options 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 ( - + {/* Header */} { Settings - + {/* Help Button */} { - + Plugins { Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers. - + {/* Current Repository */} {currentRepositoryId && ( @@ -1438,7 +1452,7 @@ const PluginsScreen: React.FC = () => { {repositoryUrl} )} - + {/* Repository List */} {repositories.length > 0 && ( @@ -1467,8 +1481,8 @@ const PluginsScreen: React.FC = () => { {repo.url} {repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'} - - + + {repo.id !== currentRepositoryId && ( { Remove - + ))} )} @@ -1541,9 +1555,9 @@ const PluginsScreen: React.FC = () => { {searchQuery.length > 0 && ( setSearchQuery('')}> - - )} - + + )} + {/* Filter Chips */} @@ -1561,7 +1575,7 @@ const PluginsScreen: React.FC = () => { selectedFilter === filter && styles.filterChipTextSelected ]}> {filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'} - + ))} @@ -1590,17 +1604,17 @@ const PluginsScreen: React.FC = () => { {filteredScrapers.length === 0 ? ( - {searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'} - + - {searchQuery + {searchQuery ? `No scrapers match "${searchQuery}". Try a different search term.` : 'Configure a repository above to view available scrapers.' } @@ -1613,45 +1627,45 @@ const PluginsScreen: React.FC = () => { Clear Search )} - - ) : ( - + + ) : ( + {filteredScrapers.map((scraper) => ( - {scraper.logo ? ( - (scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? ( - - ) : ( - - ) - ) : ( + {scraper.logo ? ( + (scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? ( + + ) : ( + + ) + ) : ( )} {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'))} - /> - + {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'))} + /> + + @@ -1682,62 +1696,62 @@ const PluginsScreen: React.FC = () => { {/* ShowBox Settings - only visible when ShowBox scraper is available */} - {showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && ( + {showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && ( - ShowBox UI Token - - 0 && !showboxTokenVisible} - multiline={false} - numberOfLines={1} - /> - {showboxSavedToken.length > 0 && ( - setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}> - - - )} - - - {showboxUiToken !== showboxSavedToken && ( - { - if (showboxScraperId) { - await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken }); - } - setShowboxSavedToken(showboxUiToken); - openAlert('Saved', 'ShowBox settings updated'); - }} - > - Save - - )} - { - setShowboxUiToken(''); - setShowboxSavedToken(''); - if (showboxScraperId) { - await pluginService.setScraperSettings(showboxScraperId, {}); - } - }} - > - Clear - - - - )} - + ShowBox UI Token + + 0 && !showboxTokenVisible} + multiline={false} + numberOfLines={1} + /> + {showboxSavedToken.length > 0 && ( + setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}> + + + )} + + + {showboxUiToken !== showboxSavedToken && ( + { + if (showboxScraperId) { + await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken }); + } + setShowboxSavedToken(showboxUiToken); + openAlert('Saved', 'ShowBox settings updated'); + }} + > + Save + + )} + { + setShowboxUiToken(''); + setShowboxSavedToken(''); + if (showboxScraperId) { + await pluginService.setScraperSettings(showboxScraperId, {}); + } + }} + > + Clear + + + + )} + ))} - - )} + + )} {/* Additional Settings */} @@ -1763,7 +1777,7 @@ const PluginsScreen: React.FC = () => { disabled={!settings.enableLocalScrapers} /> - + Group Plugin Streams @@ -1772,20 +1786,20 @@ const PluginsScreen: React.FC = () => { { - 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} - /> + value={settings.streamDisplayMode === 'grouped'} + onValueChange={(value) => { + 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 @@ -1794,14 +1808,14 @@ const PluginsScreen: React.FC = () => { 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'} - /> + value={settings.streamSortMode === 'quality-then-scraper'} + onValueChange={(value) => 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 @@ -1810,12 +1824,12 @@ const PluginsScreen: React.FC = () => { updateSetting('showScraperLogos', value)} - trackColor={{ false: colors.elevation3, true: colors.primary }} - thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} - disabled={!settings.enableLocalScrapers} - /> + value={settings.showScraperLogos && settings.enableLocalScrapers} + onValueChange={(value) => updateSetting('showScraperLogos', value)} + trackColor={{ false: colors.elevation3, true: colors.primary }} + thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'} + disabled={!settings.enableLocalScrapers} + /> @@ -1830,7 +1844,7 @@ const PluginsScreen: React.FC = () => { 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); @@ -1856,7 +1870,7 @@ const PluginsScreen: React.FC = () => { ); })} - + {(settings.excludedQualities || []).length > 0 && ( Excluded qualities: {(settings.excludedQualities || []).join(', ')} @@ -1875,11 +1889,11 @@ const PluginsScreen: React.FC = () => { Exclude specific languages from search results. Tap on a language to exclude it from plugin results. - + Note: This filter only applies to providers that include language information in their stream names. It does not affect other providers. - + {languageOptions.map((language) => { const isExcluded = (settings.excludedLanguages || []).includes(language); @@ -1905,7 +1919,7 @@ const PluginsScreen: React.FC = () => { ); })} - + {(settings.excludedLanguages || []).length > 0 && ( Excluded languages: {(settings.excludedLanguages || []).join(', ')} @@ -1988,36 +2002,36 @@ const PluginsScreen: React.FC = () => { /> - {/* Format Hint */} - - Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch - + {/* Format Hint */} + + Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch + - {/* Action Buttons */} - - { - setShowAddRepositoryModal(false); - setNewRepositoryUrl(''); - }} - > - Cancel - + {/* Action Buttons */} + + { + setShowAddRepositoryModal(false); + setNewRepositoryUrl(''); + }} + > + Cancel + - - {isLoading ? ( - - ) : ( - Add - )} - - - + + {isLoading ? ( + + ) : ( + Add + )} + + +