diff --git a/src/screens/HomeScreenSettings.tsx b/src/screens/HomeScreenSettings.tsx index 3bfb199..ea31486 100644 --- a/src/screens/HomeScreenSettings.tsx +++ b/src/screens/HomeScreenSettings.tsx @@ -339,19 +339,21 @@ const HomeScreenSettings: React.FC = () => { {settings.showHeroSection && ( <> - - Hero Layout - handleUpdateSetting('heroStyle', val as any)} - /> - Full-width banner, swipeable cards, or Apple TV style - + {!isTabletDevice && ( + + Hero Layout + handleUpdateSetting('heroStyle', val as any)} + /> + Full-width banner, swipeable cards, or Apple TV style + + )} Featured Source diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 29893ac..5a34f06 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -2,21 +2,16 @@ import { useFocusEffect } from '@react-navigation/native'; import React, { useCallback, useState, useEffect, useRef } from 'react'; import { useRealtimeConfig } from '../hooks/useRealtimeConfig'; - import { View, Text, StyleSheet, TouchableOpacity, - Switch, ScrollView, - SafeAreaView, StatusBar, Platform, Dimensions, - Button, Linking, - Clipboard } from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import { useNavigation } from '@react-navigation/native'; @@ -24,32 +19,32 @@ import { NavigationProp } from '@react-navigation/native'; import FastImage from '@d11/react-native-fast-image'; import LottieView from 'lottie-react-native'; import { Feather } from '@expo/vector-icons'; -import { Picker } from '@react-native-picker/picker'; import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings'; import { RootStackParamList } from '../navigation/AppNavigator'; import { stremioService } from '../services/stremioService'; import { useCatalogContext } from '../contexts/CatalogContext'; import { useTraktContext } from '../contexts/TraktContext'; import { useTheme } from '../contexts/ThemeContext'; -import { catalogService } from '../services/catalogService'; import { fetchTotalDownloads } from '../services/githubReleaseService'; import * as WebBrowser from 'expo-web-browser'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import * as Sentry from '@sentry/react-native'; import { getDisplayedAppVersion } from '../utils/version'; import CustomAlert from '../components/CustomAlert'; import ScreenHeader from '../components/common/ScreenHeader'; -import PluginIcon from '../components/icons/PluginIcon'; import TraktIcon from '../components/icons/TraktIcon'; -import TMDBIcon from '../components/icons/TMDBIcon'; -import MDBListIcon from '../components/icons/MDBListIcon'; import { campaignService } from '../services/campaignService'; import { useScrollToTop } from '../contexts/ScrollToTopContext'; -const { width, height } = Dimensions.get('window'); -const isTablet = width >= 768; +// Import reusable content components from settings screens +import { PlaybackSettingsContent } from './settings/PlaybackSettingsScreen'; +import { ContentDiscoverySettingsContent } from './settings/ContentDiscoverySettingsScreen'; +import { AppearanceSettingsContent } from './settings/AppearanceSettingsScreen'; +import { IntegrationsSettingsContent } from './settings/IntegrationsSettingsScreen'; +import { AboutSettingsContent, AboutFooter } from './settings/AboutSettingsScreen'; +import { SettingsCard, SettingItem, ChevronRight, CustomSwitch } from './settings/SettingsComponents'; -const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; +const { width } = Dimensions.get('window'); +const isTablet = width >= 768; // Settings categories for tablet sidebar const SETTINGS_CATEGORIES = [ @@ -57,7 +52,6 @@ const SETTINGS_CATEGORIES = [ { id: 'content', title: 'Content & Discovery', icon: 'compass' as string }, { id: 'appearance', title: 'Appearance', icon: 'sliders' as string }, { id: 'integrations', title: 'Integrations', icon: 'layers' as string }, - { id: 'ai', title: 'AI Assistant', icon: 'cpu' as string }, { id: 'playback', title: 'Playback', icon: 'play-circle' as string }, { id: 'backup', title: 'Backup & Restore', icon: 'archive' as string }, { id: 'updates', title: 'Updates', icon: 'refresh-ccw' as string }, @@ -66,134 +60,6 @@ const SETTINGS_CATEGORIES = [ { id: 'cache', title: 'Cache', icon: 'database' as string }, ]; -// Card component with minimalistic style -interface SettingsCardProps { - children: React.ReactNode; - title?: string; - isTablet?: boolean; -} - -const SettingsCard: React.FC = ({ children, title, isTablet = false }) => { - const { currentTheme } = useTheme(); - - return ( - - {title && ( - - {title} - - )} - - {children} - - - ); -}; - -interface SettingItemProps { - title: string; - description?: string; - icon?: string; - customIcon?: React.ReactNode; - renderControl?: () => React.ReactNode; - isLast?: boolean; - onPress?: () => void; - badge?: string | number; - isTablet?: boolean; -} - -const SettingItem: React.FC = ({ - title, - description, - icon, - customIcon, - renderControl, - isLast = false, - onPress, - badge, - isTablet = false -}) => { - const { currentTheme } = useTheme(); - - return ( - - - {customIcon ? ( - customIcon - ) : ( - - )} - - - - - {title} - - {description && ( - - {description} - - )} - - {badge && ( - - {String(badge)} - - )} - - {renderControl && ( - - {renderControl()} - - )} - - ); -}; - // Tablet Sidebar Component interface SidebarProps { selectedCategory: string; @@ -306,6 +172,7 @@ const SettingsScreen: React.FC = () => { })(); return () => { mounted = false; }; }, []); + const navigation = useNavigation>(); const { lastUpdate } = useCatalogContext(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); @@ -316,13 +183,9 @@ const SettingsScreen: React.FC = () => { const [selectedCategory, setSelectedCategory] = useState('account'); // States for dynamic content - const [addonCount, setAddonCount] = useState(0); - const [catalogCount, setCatalogCount] = useState(0); const [mdblistKeySet, setMdblistKeySet] = useState(false); - const [openRouterKeySet, setOpenRouterKeySet] = useState(false); const [totalDownloads, setTotalDownloads] = useState(0); const [displayDownloads, setDisplayDownloads] = useState(null); - const [isCountingUp, setIsCountingUp] = useState(false); // Use Realtime Config Hook const settingsConfig = useRealtimeConfig(); @@ -338,91 +201,45 @@ const SettingsScreen: React.FC = () => { useScrollToTop('Settings', scrollToTop); - // Add a useEffect to check Trakt authentication status on focus + // Refresh Trakt auth status on focus useEffect(() => { - // This will reload the Trakt auth status whenever the settings screen is focused const unsubscribe = navigation.addListener('focus', () => { - // Force a re-render when returning to this screen - // This will reflect the updated isAuthenticated state from the TraktContext - // Refresh auth status - if (isAuthenticated || userProfile) { - // Just to be cautious, log the current state - if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username }); - } refreshAuthStatus(); }); - return unsubscribe; - }, [navigation, isAuthenticated, userProfile, refreshAuthStatus]); + }, [navigation, refreshAuthStatus]); const loadData = useCallback(async () => { try { - // Load addon count and get their catalogs - const addons = await stremioService.getInstalledAddonsAsync(); - setAddonCount(addons.length); - - // Count total available catalogs - let totalCatalogs = 0; - addons.forEach(addon => { - if (addon.catalogs && addon.catalogs.length > 0) { - totalCatalogs += addon.catalogs.length; - } - }); - - // Load saved catalog settings - const catalogSettingsJson = await mmkvStorage.getItem('catalog_settings'); - if (catalogSettingsJson) { - const catalogSettings = JSON.parse(catalogSettingsJson); - // Filter out _lastUpdate key and count only explicitly disabled catalogs - const disabledCount = Object.entries(catalogSettings) - .filter(([key, value]) => key !== '_lastUpdate' && value === false) - .length; - // Since catalogs are enabled by default, subtract disabled ones from total - setCatalogCount(totalCatalogs - disabledCount); - } else { - // If no settings saved, all catalogs are enabled by default - setCatalogCount(totalCatalogs); - } - // Check MDBList API key status const mdblistKey = await mmkvStorage.getItem('mdblist_api_key'); setMdblistKeySet(!!mdblistKey); - // Check OpenRouter API key status - const openRouterKey = await mmkvStorage.getItem('openrouter_api_key'); - setOpenRouterKeySet(!!openRouterKey); - - // Load GitHub total downloads (initial load only, polling happens in useEffect) + // Load GitHub total downloads const downloads = await fetchTotalDownloads(); if (downloads !== null) { setTotalDownloads(downloads); setDisplayDownloads(downloads); } - } catch (error) { if (__DEV__) console.error('Error loading settings data:', error); } }, []); - // Load data initially and when catalogs are updated useEffect(() => { loadData(); }, [loadData, lastUpdate]); - // Add focus listener to reload data when screen comes into focus useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { loadData(); }); - return unsubscribe; }, [navigation, loadData]); - // Poll GitHub downloads every 10 seconds when on the About section + // Poll GitHub downloads useEffect(() => { - // Only poll when viewing the About section (where downloads counter is shown) const shouldPoll = isTablet ? selectedCategory === 'about' : true; - if (!shouldPoll) return; const pollInterval = setInterval(async () => { @@ -434,28 +251,25 @@ const SettingsScreen: React.FC = () => { } catch (error) { if (__DEV__) console.error('Error polling downloads:', error); } - }, 3600000); // 3600000 milliseconds (1 hour) + }, 3600000); return () => clearInterval(pollInterval); - }, [selectedCategory, isTablet, totalDownloads]); + }, [selectedCategory, totalDownloads]); // Animate counting up when totalDownloads changes useEffect(() => { if (totalDownloads === null || displayDownloads === null) return; if (totalDownloads === displayDownloads) return; - setIsCountingUp(true); const start = displayDownloads; const end = totalDownloads; - const duration = 2000; // 2 seconds animation + const duration = 2000; const startTime = Date.now(); const animate = () => { const now = Date.now(); const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); - - // Ease out quad for smooth deceleration const easeProgress = 1 - Math.pow(1 - progress, 2); const current = Math.floor(start + (end - start) * easeProgress); @@ -465,31 +279,12 @@ const SettingsScreen: React.FC = () => { requestAnimationFrame(animate); } else { setDisplayDownloads(end); - setIsCountingUp(false); } }; requestAnimationFrame(animate); }, [totalDownloads]); - const handleResetSettings = useCallback(() => { - openAlert( - 'Reset Settings', - 'Are you sure you want to reset all settings to default values?', - [ - { label: 'Cancel', onPress: () => { } }, - { - label: 'Reset', - onPress: () => { - (Object.keys(DEFAULT_SETTINGS) as Array).forEach(key => { - updateSetting(key, DEFAULT_SETTINGS[key]); - }); - } - } - ] - ); - }, [updateSetting]); - const handleClearMDBListCache = () => { openAlert( 'Clear MDBList Cache', @@ -512,24 +307,6 @@ const SettingsScreen: React.FC = () => { ); }; - const CustomSwitch = ({ value, onValueChange }: { value: boolean, onValueChange: (value: boolean) => void }) => ( - - ); - - const ChevronRight = () => ( - - ); - // Helper to check item visibility const isItemVisible = (itemId: string) => { if (!settingsConfig?.items) return true; @@ -546,8 +323,7 @@ const SettingsScreen: React.FC = () => { return true; }); - - + // Render tablet category content using reusable components const renderCategoryContent = (categoryId: string) => { switch (categoryId) { case 'account': @@ -558,7 +334,7 @@ const SettingsScreen: React.FC = () => { title="Trakt" description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"} customIcon={} - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} isLast={true} isTablet={isTablet} @@ -568,272 +344,19 @@ const SettingsScreen: React.FC = () => { ); case 'content': - return ( - - {isItemVisible('addons') && ( - navigation.navigate('Addons')} - isTablet={isTablet} - /> - )} - {isItemVisible('debrid') && ( - navigation.navigate('DebridIntegration')} - isTablet={isTablet} - /> - )} - {isItemVisible('plugins') && ( - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('ScraperSettings')} - isTablet={isTablet} - /> - )} - {isItemVisible('catalogs') && ( - navigation.navigate('CatalogSettings')} - isTablet={isTablet} - /> - )} - {isItemVisible('home_screen') && ( - navigation.navigate('HomeScreenSettings')} - isTablet={isTablet} - /> - )} - {isItemVisible('show_discover') && ( - ( - updateSetting('showDiscover', value)} - /> - )} - isTablet={isTablet} - /> - )} - {isItemVisible('continue_watching') && ( - navigation.navigate('ContinueWatchingSettings')} - isLast={true} - isTablet={isTablet} - /> - )} - - ); + return ; case 'appearance': - return ( - - {isItemVisible('theme') && ( - navigation.navigate('ThemeSettings')} - isTablet={isTablet} - /> - )} - {isItemVisible('episode_layout') && ( - ( - updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} - /> - )} - isLast={isTablet} - isTablet={isTablet} - /> - )} - {!isTablet && isItemVisible('streams_backdrop') && ( - ( - updateSetting('enableStreamsBackdrop', value)} - /> - )} - isLast={true} - isTablet={isTablet} - /> - )} - - ); + return ; case 'integrations': - return ( - - {isItemVisible('mdblist') && ( - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('MDBListSettings')} - isTablet={isTablet} - /> - )} - {isItemVisible('tmdb') && ( - } - renderControl={ChevronRight} - onPress={() => navigation.navigate('TMDBSettings')} - isLast={true} - isTablet={isTablet} - /> - )} - - ); - - case 'ai': - return ( - - {isItemVisible('openrouter') && ( - navigation.navigate('AISettings')} - isLast={true} - isTablet={isTablet} - /> - )} - - ); + return ; case 'playback': - return ( - - {isItemVisible('video_player') && ( - navigation.navigate('PlayerSettings')} - isTablet={isTablet} - /> - )} - {isItemVisible('show_trailers') && ( - ( - updateSetting('showTrailers', value)} - trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} - thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'} - /> - )} - isTablet={isTablet} - /> - )} - {isItemVisible('enable_downloads') && ( - ( - updateSetting('enableDownloads', value)} - trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} - thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} - /> - )} - isTablet={isTablet} - /> - )} - {isItemVisible('notifications') && ( - navigation.navigate('NotificationSettings')} - isLast={true} - isTablet={isTablet} - /> - )} - - ); + return ; case 'about': - return ( - - Linking.openURL('https://tapframe.github.io/NuvioStreaming/#privacy-policy')} - renderControl={ChevronRight} - isTablet={isTablet} - /> - Sentry.showFeedbackWidget()} - renderControl={ChevronRight} - isTablet={isTablet} - /> - - navigation.navigate('Contributors')} - isLast={true} - isTablet={isTablet} - /> - - ); + return ; case 'developer': return __DEV__ ? ( @@ -842,7 +365,7 @@ const SettingsScreen: React.FC = () => { title="Test Onboarding" icon="play-circle" onPress={() => navigation.navigate('Onboarding')} - renderControl={ChevronRight} + renderControl={() => } isTablet={isTablet} /> { openAlert('Error', 'Failed to reset onboarding.'); } }} - renderControl={ChevronRight} + renderControl={() => } isTablet={isTablet} /> { openAlert('Error', 'Failed to reset announcement.'); } }} - renderControl={ChevronRight} + renderControl={() => } isTablet={isTablet} /> { await campaignService.resetCampaigns(); openAlert('Success', 'Campaign history reset. Restart app to see posters again.'); }} - renderControl={ChevronRight} + renderControl={() => } isTablet={isTablet} /> { title="Backup & Restore" description="Create and restore app backups" icon="archive" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('Backup')} isLast={true} isTablet={isTablet} @@ -949,7 +472,7 @@ const SettingsScreen: React.FC = () => { title="App Updates" description="Check for updates and manage app version" icon="refresh-ccw" - renderControl={ChevronRight} + renderControl={() => } badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined} onPress={async () => { if (Platform.OS === 'android') { @@ -969,15 +492,13 @@ const SettingsScreen: React.FC = () => { } }; - // Keep headers below floating top navigator on tablets by adding extra offset + // Keep headers below floating top navigator on tablets const tabletNavOffset = isTablet ? 64 : 0; + // TABLET LAYOUT if (isTablet) { return ( - + { {renderCategoryContent(selectedCategory)} {selectedCategory === 'about' && ( - <> - {displayDownloads !== null && ( - - - {displayDownloads.toLocaleString()} - - - downloads and counting - - - )} - - - WebBrowser.openBrowserAsync('https://ko-fi.com/tapframe', { - presentationStyle: Platform.OS === 'ios' ? WebBrowser.WebBrowserPresentationStyle.FORM_SHEET : WebBrowser.WebBrowserPresentationStyle.FORM_SHEET - })} - activeOpacity={0.7} - > - - - - - Linking.openURL('https://discord.gg/KVgDTjhA4H')} - activeOpacity={0.7} - > - - - - Discord - - - - - Linking.openURL('https://www.reddit.com/r/Nuvio/')} - activeOpacity={0.7} - > - - - - Reddit - - - - - - - {/* Monkey Animation */} - - - - - - - - - - - Made with ❤️ by Tapframe and Friends - - - + )} @@ -1107,18 +540,12 @@ const SettingsScreen: React.FC = () => { ); } - // Mobile Layout - Simplified navigation hub + // MOBILE LAYOUT - Simplified navigation hub return ( - + - + - { title="Trakt" description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"} customIcon={} - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} isLast /> @@ -1155,7 +582,7 @@ const SettingsScreen: React.FC = () => { title="Content & Discovery" description="Addons, catalogs, and sources" icon="compass" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('ContentDiscoverySettings')} /> )} @@ -1164,7 +591,7 @@ const SettingsScreen: React.FC = () => { title="Appearance" description={currentTheme.name} icon="sliders" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('AppearanceSettings')} /> )} @@ -1173,7 +600,7 @@ const SettingsScreen: React.FC = () => { title="Integrations" description="MDBList, TMDB, AI" icon="layers" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('IntegrationsSettings')} /> )} @@ -1182,7 +609,7 @@ const SettingsScreen: React.FC = () => { title="Playback" description="Player, trailers, downloads" icon="play-circle" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('PlaybackSettings')} isLast /> @@ -1201,7 +628,7 @@ const SettingsScreen: React.FC = () => { title="Backup & Restore" description="Create and restore app backups" icon="archive" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('Backup')} /> )} @@ -1211,7 +638,7 @@ const SettingsScreen: React.FC = () => { description="Check for updates" icon="refresh-ccw" badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined} - renderControl={ChevronRight} + renderControl={() => } onPress={async () => { if (Platform.OS === 'android') { try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } @@ -1243,7 +670,7 @@ const SettingsScreen: React.FC = () => { title="About Nuvio" description={getDisplayedAppVersion()} icon="info" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('AboutSettings')} isLast /> @@ -1256,7 +683,7 @@ const SettingsScreen: React.FC = () => { title="Developer Tools" description="Testing and debug options" icon="code" - renderControl={ChevronRight} + renderControl={() => } onPress={() => navigation.navigate('DeveloperSettings')} isLast /> @@ -1388,7 +815,6 @@ const styles = StyleSheet.create({ paddingTop: 8, paddingBottom: 32, }, - // Tablet-specific styles tabletContainer: { flex: 1, @@ -1449,139 +875,7 @@ const styles = StyleSheet.create({ paddingTop: 8, paddingBottom: 40, }, - - // Common card styles - cardContainer: { - width: '100%', - marginBottom: 24, - }, - tabletCardContainer: { - marginBottom: 28, - }, - cardTitle: { - fontSize: 12, - fontWeight: '600', - letterSpacing: 1, - marginLeft: Math.max(16, width * 0.045), - marginBottom: 10, - textTransform: 'uppercase', - }, - tabletCardTitle: { - fontSize: 12, - marginLeft: 4, - marginBottom: 12, - }, - card: { - marginHorizontal: Math.max(16, width * 0.04), - borderRadius: 14, - overflow: 'hidden', - width: undefined, - }, - tabletCard: { - marginHorizontal: 0, - borderRadius: 16, - }, - settingItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 14, - paddingHorizontal: Math.max(14, width * 0.04), - borderBottomWidth: StyleSheet.hairlineWidth, - minHeight: Math.max(60, width * 0.15), - width: '100%', - }, - tabletSettingItem: { - paddingVertical: 16, - paddingHorizontal: 20, - minHeight: 68, - }, - settingItemBorder: { - // Border styling handled directly in the component with borderBottomWidth - }, - settingIconContainer: { - marginRight: 14, - width: 38, - height: 38, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - }, - tabletSettingIconContainer: { - width: 42, - height: 42, - borderRadius: 11, - marginRight: 16, - }, - settingContent: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - }, - settingTextContainer: { - flex: 1, - }, - settingTitle: { - fontSize: Math.min(16, width * 0.04), - fontWeight: '500', - marginBottom: 2, - letterSpacing: -0.2, - }, - tabletSettingTitle: { - fontSize: 17, - fontWeight: '500', - marginBottom: 3, - }, - settingDescription: { - fontSize: Math.min(13, width * 0.034), - opacity: 0.7, - }, - tabletSettingDescription: { - fontSize: 14, - opacity: 0.6, - }, - settingControl: { - justifyContent: 'center', - alignItems: 'center', - paddingLeft: 10, - }, - badge: { - height: 20, - minWidth: 20, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 6, - marginRight: 8, - }, - badgeText: { - color: 'white', - fontSize: 11, - fontWeight: '700', - }, - segmentedControl: { - flexDirection: 'row', - backgroundColor: 'rgba(255,255,255,0.08)', - borderRadius: 8, - padding: 2, - }, - segment: { - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 6, - minWidth: 60, - alignItems: 'center', - }, - segmentActive: { - backgroundColor: 'rgba(255,255,255,0.16)', - }, - segmentText: { - fontSize: 13, - fontWeight: '500', - }, - segmentTextActive: { - color: 'white', - fontWeight: '600', - }, + // Footer and social styles footer: { alignItems: 'center', justifyContent: 'center', @@ -1593,7 +887,6 @@ const styles = StyleSheet.create({ opacity: 0.5, letterSpacing: 0.2, }, - // Support buttons discordContainer: { marginTop: 12, marginBottom: 24, @@ -1643,14 +936,6 @@ const styles = StyleSheet.create({ letterSpacing: 1.5, textTransform: 'uppercase', }, - loadingSpinner: { - width: 16, - height: 16, - borderWidth: 2, - borderRadius: 8, - borderTopColor: 'transparent', - marginRight: 8, - }, monkeyContainer: { alignItems: 'center', justifyContent: 'center', diff --git a/src/screens/settings/AboutSettingsScreen.tsx b/src/screens/settings/AboutSettingsScreen.tsx index f5b0e56..35a8324 100644 --- a/src/screens/settings/AboutSettingsScreen.tsx +++ b/src/screens/settings/AboutSettingsScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, StyleSheet, ScrollView, StatusBar, TouchableOpacity, Platform, Linking } from 'react-native'; +import { View, Text, StyleSheet, ScrollView, StatusBar, TouchableOpacity, Platform, Linking, Dimensions } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -14,53 +14,186 @@ import { getDisplayedAppVersion } from '../../utils/version'; import ScreenHeader from '../../components/common/ScreenHeader'; import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents'; +const { width } = Dimensions.get('window'); + +interface AboutSettingsContentProps { + isTablet?: boolean; + displayDownloads?: number | null; +} + +/** + * Reusable AboutSettingsContent component + * Can be used inline (tablets) or wrapped in a screen (mobile) + */ +export const AboutSettingsContent: React.FC = ({ + isTablet = false, + displayDownloads: externalDisplayDownloads +}) => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + + const [internalDisplayDownloads, setInternalDisplayDownloads] = useState(null); + + // Use external downloads if provided (for tablet inline use), otherwise load internally + const displayDownloads = externalDisplayDownloads ?? internalDisplayDownloads; + + useEffect(() => { + // Only load downloads internally if not provided externally + if (externalDisplayDownloads === undefined) { + const loadDownloads = async () => { + const downloads = await fetchTotalDownloads(); + if (downloads !== null) { + setInternalDisplayDownloads(downloads); + } + }; + loadDownloads(); + } + }, [externalDisplayDownloads]); + + return ( + <> + + Linking.openURL('https://tapframe.github.io/NuvioStreaming/#privacy-policy')} + renderControl={() => } + isTablet={isTablet} + /> + Sentry.showFeedbackWidget()} + renderControl={() => } + isTablet={isTablet} + /> + + } + onPress={() => navigation.navigate('Contributors')} + isLast + isTablet={isTablet} + /> + + + ); +}; + +/** + * Reusable AboutFooter component - Downloads counter, social links, branding + */ +export const AboutFooter: React.FC<{ displayDownloads: number | null }> = ({ displayDownloads }) => { + const { currentTheme } = useTheme(); + + return ( + <> + {displayDownloads !== null && ( + + + {displayDownloads.toLocaleString()} + + + downloads and counting + + + )} + + + WebBrowser.openBrowserAsync('https://ko-fi.com/tapframe', { + presentationStyle: Platform.OS === 'ios' ? WebBrowser.WebBrowserPresentationStyle.FORM_SHEET : WebBrowser.WebBrowserPresentationStyle.FORM_SHEET + })} + activeOpacity={0.7} + > + + + + + Linking.openURL('https://discord.gg/KVgDTjhA4H')} + activeOpacity={0.7} + > + + + + Discord + + + + + Linking.openURL('https://www.reddit.com/r/Nuvio/')} + activeOpacity={0.7} + > + + + + Reddit + + + + + + + {/* Monkey Animation */} + + + + + + + + + + + Made with ❤️ by Tapframe and Friends + + + + ); +}; + +/** + * AboutSettingsScreen - Wrapper for mobile navigation + */ const AboutSettingsScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - - const [totalDownloads, setTotalDownloads] = useState(null); - const [displayDownloads, setDisplayDownloads] = useState(null); - - useEffect(() => { - const loadDownloads = async () => { - const downloads = await fetchTotalDownloads(); - if (downloads !== null) { - setTotalDownloads(downloads); - setDisplayDownloads(downloads); - } - }; - loadDownloads(); - }, []); - - // Animate counting up when totalDownloads changes - useEffect(() => { - if (totalDownloads === null || displayDownloads === null) return; - if (totalDownloads === displayDownloads) return; - - const start = displayDownloads; - const end = totalDownloads; - const duration = 2000; - const startTime = Date.now(); - - const animate = () => { - const now = Date.now(); - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const easeProgress = 1 - Math.pow(1 - progress, 2); - const current = Math.floor(start + (end - start) * easeProgress); - - setDisplayDownloads(current); - - if (progress < 1) { - requestAnimationFrame(animate); - } else { - setDisplayDownloads(end); - } - }; - - requestAnimationFrame(animate); - }, [totalDownloads]); + const screenIsTablet = width >= 768; return ( @@ -72,34 +205,7 @@ const AboutSettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 40 }]} > - - Linking.openURL('https://tapframe.github.io/NuvioStreaming/#privacy-policy')} - renderControl={() => } - /> - Sentry.showFeedbackWidget()} - renderControl={() => } - /> - - } - onPress={() => navigation.navigate('Contributors')} - isLast - /> - - + diff --git a/src/screens/settings/AppearanceSettingsScreen.tsx b/src/screens/settings/AppearanceSettingsScreen.tsx index 5a67e02..5c1d002 100644 --- a/src/screens/settings/AppearanceSettingsScreen.tsx +++ b/src/screens/settings/AppearanceSettingsScreen.tsx @@ -1,6 +1,6 @@ -import React, { useState, useCallback } from 'react'; -import { View, StyleSheet, ScrollView, StatusBar, Platform, Dimensions } from 'react-native'; -import { useNavigation, useFocusEffect } from '@react-navigation/native'; +import React from 'react'; +import { View, StyleSheet, ScrollView, StatusBar, Dimensions } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../../contexts/ThemeContext'; @@ -11,13 +11,19 @@ import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './Setting import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; const { width } = Dimensions.get('window'); -const isTablet = width >= 768; -const AppearanceSettingsScreen: React.FC = () => { +interface AppearanceSettingsContentProps { + isTablet?: boolean; +} + +/** + * Reusable AppearanceSettingsContent component + * Can be used inline (tablets) or wrapped in a screen (mobile) + */ +export const AppearanceSettingsContent: React.FC = ({ isTablet = false }) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings, updateSetting } = useSettings(); - const insets = useSafeAreaInsets(); const config = useRealtimeConfig(); const isItemVisible = (itemId: string) => { @@ -34,6 +40,71 @@ const AppearanceSettingsScreen: React.FC = () => { }); }; + return ( + <> + {hasVisibleItems(['theme']) && ( + + {isItemVisible('theme') && ( + } + onPress={() => navigation.navigate('ThemeSettings')} + isLast + isTablet={isTablet} + /> + )} + + )} + + {hasVisibleItems(['episode_layout', 'streams_backdrop']) && ( + + {isItemVisible('episode_layout') && ( + ( + updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} + /> + )} + isLast={isTablet || !isItemVisible('streams_backdrop')} + isTablet={isTablet} + /> + )} + {!isTablet && isItemVisible('streams_backdrop') && ( + ( + updateSetting('enableStreamsBackdrop', value)} + /> + )} + isLast + isTablet={isTablet} + /> + )} + + )} + + ); +}; + +/** + * AppearanceSettingsScreen - Wrapper for mobile navigation + */ +const AppearanceSettingsScreen: React.FC = () => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + const screenIsTablet = width >= 768; + return ( @@ -44,53 +115,7 @@ const AppearanceSettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - {hasVisibleItems(['theme']) && ( - - {isItemVisible('theme') && ( - } - onPress={() => navigation.navigate('ThemeSettings')} - isLast - /> - )} - - )} - - {hasVisibleItems(['episode_layout', 'streams_backdrop']) && ( - - {isItemVisible('episode_layout') && ( - ( - updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} - /> - )} - isLast={isTablet || !isItemVisible('streams_backdrop')} - /> - )} - {!isTablet && isItemVisible('streams_backdrop') && ( - ( - updateSetting('enableStreamsBackdrop', value)} - /> - )} - isLast - /> - )} - - )} + ); diff --git a/src/screens/settings/ContentDiscoverySettingsScreen.tsx b/src/screens/settings/ContentDiscoverySettingsScreen.tsx index a928799..9ef8c71 100644 --- a/src/screens/settings/ContentDiscoverySettingsScreen.tsx +++ b/src/screens/settings/ContentDiscoverySettingsScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { View, StyleSheet, ScrollView, StatusBar, Platform } from 'react-native'; +import { View, StyleSheet, ScrollView, StatusBar, Platform, Dimensions } from 'react-native'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -13,15 +13,24 @@ import PluginIcon from '../../components/icons/PluginIcon'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; -const ContentDiscoverySettingsScreen: React.FC = () => { +const { width } = Dimensions.get('window'); + +interface ContentDiscoverySettingsContentProps { + isTablet?: boolean; +} + +/** + * Reusable ContentDiscoverySettingsContent component + * Can be used inline (tablets) or wrapped in a screen (mobile) + */ +export const ContentDiscoverySettingsContent: React.FC = ({ isTablet = false }) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings, updateSetting } = useSettings(); - const insets = useSafeAreaInsets(); + const config = useRealtimeConfig(); const [addonCount, setAddonCount] = useState(0); const [catalogCount, setCatalogCount] = useState(0); - const config = useRealtimeConfig(); const loadData = useCallback(async () => { try { @@ -67,6 +76,112 @@ const ContentDiscoverySettingsScreen: React.FC = () => { return itemIds.some(id => isItemVisible(id)); }; + return ( + <> + {hasVisibleItems(['addons', 'debrid', 'plugins']) && ( + + {isItemVisible('addons') && ( + } + onPress={() => navigation.navigate('Addons')} + isTablet={isTablet} + /> + )} + {isItemVisible('debrid') && ( + } + onPress={() => navigation.navigate('DebridIntegration')} + isTablet={isTablet} + /> + )} + {isItemVisible('plugins') && ( + } + renderControl={() => } + onPress={() => navigation.navigate('ScraperSettings')} + isLast + isTablet={isTablet} + /> + )} + + )} + + {hasVisibleItems(['catalogs', 'home_screen', 'continue_watching']) && ( + + {isItemVisible('catalogs') && ( + } + onPress={() => navigation.navigate('CatalogSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('home_screen') && ( + } + onPress={() => navigation.navigate('HomeScreenSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('continue_watching') && ( + } + onPress={() => navigation.navigate('ContinueWatchingSettings')} + isLast + isTablet={isTablet} + /> + )} + + )} + + {hasVisibleItems(['show_discover']) && ( + + {isItemVisible('show_discover') && ( + ( + updateSetting('showDiscover', value)} + /> + )} + isLast + isTablet={isTablet} + /> + )} + + )} + + ); +}; + +/** + * ContentDiscoverySettingsScreen - Wrapper for mobile navigation + */ +const ContentDiscoverySettingsScreen: React.FC = () => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + const screenIsTablet = width >= 768; + return ( @@ -77,90 +192,7 @@ const ContentDiscoverySettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - {hasVisibleItems(['addons', 'debrid', 'plugins']) && ( - - {isItemVisible('addons') && ( - } - onPress={() => navigation.navigate('Addons')} - /> - )} - {isItemVisible('debrid') && ( - } - onPress={() => navigation.navigate('DebridIntegration')} - /> - )} - {isItemVisible('plugins') && ( - } - renderControl={() => } - onPress={() => navigation.navigate('ScraperSettings')} - isLast - /> - )} - - )} - - {hasVisibleItems(['catalogs', 'home_screen', 'continue_watching']) && ( - - {isItemVisible('catalogs') && ( - } - onPress={() => navigation.navigate('CatalogSettings')} - /> - )} - {isItemVisible('home_screen') && ( - } - onPress={() => navigation.navigate('HomeScreenSettings')} - /> - )} - {isItemVisible('continue_watching') && ( - } - onPress={() => navigation.navigate('ContinueWatchingSettings')} - isLast - /> - )} - - )} - - {hasVisibleItems(['show_discover']) && ( - - {isItemVisible('show_discover') && ( - ( - updateSetting('showDiscover', value)} - /> - )} - isLast - /> - )} - - )} + ); diff --git a/src/screens/settings/IntegrationsSettingsScreen.tsx b/src/screens/settings/IntegrationsSettingsScreen.tsx index 887caee..6af002f 100644 --- a/src/screens/settings/IntegrationsSettingsScreen.tsx +++ b/src/screens/settings/IntegrationsSettingsScreen.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { View, StyleSheet, ScrollView, StatusBar } from 'react-native'; +import React, { useState, useCallback } from 'react'; +import { View, StyleSheet, ScrollView, StatusBar, Dimensions } from 'react-native'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -12,14 +12,23 @@ import TMDBIcon from '../../components/icons/TMDBIcon'; import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents'; import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; -const IntegrationsSettingsScreen: React.FC = () => { +const { width } = Dimensions.get('window'); + +interface IntegrationsSettingsContentProps { + isTablet?: boolean; +} + +/** + * Reusable IntegrationsSettingsContent component + * Can be used inline (tablets) or wrapped in a screen (mobile) + */ +export const IntegrationsSettingsContent: React.FC = ({ isTablet = false }) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); - const insets = useSafeAreaInsets(); + const config = useRealtimeConfig(); const [mdblistKeySet, setMdblistKeySet] = useState(false); const [openRouterKeySet, setOpenRouterKeySet] = useState(false); - const config = useRealtimeConfig(); const loadData = useCallback(async () => { try { @@ -50,6 +59,62 @@ const IntegrationsSettingsScreen: React.FC = () => { return itemIds.some(id => isItemVisible(id)); }; + return ( + <> + {hasVisibleItems(['mdblist', 'tmdb']) && ( + + {isItemVisible('mdblist') && ( + } + renderControl={() => } + onPress={() => navigation.navigate('MDBListSettings')} + isTablet={isTablet} + /> + )} + {isItemVisible('tmdb') && ( + } + renderControl={() => } + onPress={() => navigation.navigate('TMDBSettings')} + isLast + isTablet={isTablet} + /> + )} + + )} + + {hasVisibleItems(['openrouter']) && ( + + {isItemVisible('openrouter') && ( + } + onPress={() => navigation.navigate('AISettings')} + isLast + isTablet={isTablet} + /> + )} + + )} + + ); +}; + +/** + * IntegrationsSettingsScreen - Wrapper for mobile navigation + */ +const IntegrationsSettingsScreen: React.FC = () => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + const screenIsTablet = width >= 768; + return ( @@ -60,44 +125,7 @@ const IntegrationsSettingsScreen: React.FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} > - {hasVisibleItems(['mdblist', 'tmdb']) && ( - - {isItemVisible('mdblist') && ( - } - renderControl={() => } - onPress={() => navigation.navigate('MDBListSettings')} - /> - )} - {isItemVisible('tmdb') && ( - } - renderControl={() => } - onPress={() => navigation.navigate('TMDBSettings')} - isLast - /> - )} - - )} - - {hasVisibleItems(['openrouter']) && ( - - {isItemVisible('openrouter') && ( - } - onPress={() => navigation.navigate('AISettings')} - isLast - /> - )} - - )} + ); diff --git a/src/screens/settings/PlaybackSettingsScreen.tsx b/src/screens/settings/PlaybackSettingsScreen.tsx index 892aa07..0ebf343 100644 --- a/src/screens/settings/PlaybackSettingsScreen.tsx +++ b/src/screens/settings/PlaybackSettingsScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useMemo, useRef } from 'react'; -import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity } from 'react-native'; +import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions } from 'react-native'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -12,6 +12,8 @@ import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; import { MaterialIcons } from '@expo/vector-icons'; import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; +const { width } = Dimensions.get('window'); + // Available languages for audio/subtitle selection const AVAILABLE_LANGUAGES = [ { code: 'en', name: 'English' }, @@ -54,11 +56,19 @@ const SUBTITLE_SOURCE_OPTIONS = [ { value: 'any', label: 'Any Available', description: 'Use first available subtitle track' }, ]; -const PlaybackSettingsScreen: React.FC = () => { +// Props for the reusable content component +interface PlaybackSettingsContentProps { + isTablet?: boolean; +} + +/** + * Reusable PlaybackSettingsContent component + * Can be used inline (tablets) or wrapped in a screen (mobile) + */ +export const PlaybackSettingsContent: React.FC = ({ isTablet = false }) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings, updateSetting } = useSettings(); - const insets = useSafeAreaInsets(); const config = useRealtimeConfig(); // Bottom sheet refs @@ -139,117 +149,116 @@ const PlaybackSettingsScreen: React.FC = () => { }; return ( - - - navigation.goBack()} /> - - - {hasVisibleItems(['video_player']) && ( - - {isItemVisible('video_player') && ( - } - onPress={() => navigation.navigate('PlayerSettings')} - isLast - /> - )} - - )} - - {/* Audio & Subtitle Preferences */} - - } - onPress={openAudioLanguageSheet} - /> - } - onPress={openSubtitleLanguageSheet} - /> - } - onPress={openSubtitleSourceSheet} - /> - ( - updateSetting('enableSubtitleAutoSelect', value)} - /> - )} - isLast - /> + <> + {hasVisibleItems(['video_player']) && ( + + {isItemVisible('video_player') && ( + } + onPress={() => navigation.navigate('PlayerSettings')} + isLast + isTablet={isTablet} + /> + )} + )} - {hasVisibleItems(['show_trailers', 'enable_downloads']) && ( - - {isItemVisible('show_trailers') && ( - ( - updateSetting('showTrailers', value)} - /> - )} - /> - )} - {isItemVisible('enable_downloads') && ( - ( - updateSetting('enableDownloads', value)} - /> - )} - isLast - /> - )} - - )} + {/* Audio & Subtitle Preferences */} + + } + onPress={openAudioLanguageSheet} + isTablet={isTablet} + /> + } + onPress={openSubtitleLanguageSheet} + isTablet={isTablet} + /> + } + onPress={openSubtitleSourceSheet} + isTablet={isTablet} + /> + ( + updateSetting('enableSubtitleAutoSelect', value)} + /> + )} + isLast + isTablet={isTablet} + /> + - {hasVisibleItems(['notifications']) && ( - - {isItemVisible('notifications') && ( - } - onPress={() => navigation.navigate('NotificationSettings')} - isLast - /> - )} - - )} - + {hasVisibleItems(['show_trailers', 'enable_downloads']) && ( + + {isItemVisible('show_trailers') && ( + ( + updateSetting('showTrailers', value)} + /> + )} + isTablet={isTablet} + /> + )} + {isItemVisible('enable_downloads') && ( + ( + updateSetting('enableDownloads', value)} + /> + )} + isLast + isTablet={isTablet} + /> + )} + + )} + + {hasVisibleItems(['notifications']) && ( + + {isItemVisible('notifications') && ( + } + onPress={() => navigation.navigate('NotificationSettings')} + isLast + isTablet={isTablet} + /> + )} + + )} {/* Audio Language Bottom Sheet */} { })} + + ); +}; + +/** + * PlaybackSettingsScreen - Wrapper for mobile navigation + * Uses PlaybackSettingsContent internally + */ +const PlaybackSettingsScreen: React.FC = () => { + const navigation = useNavigation>(); + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + const screenIsTablet = width >= 768; + + return ( + + + navigation.goBack()} /> + + + + ); }; diff --git a/src/screens/settings/index.ts b/src/screens/settings/index.ts index f914786..e615c2e 100644 --- a/src/screens/settings/index.ts +++ b/src/screens/settings/index.ts @@ -1,7 +1,17 @@ +// Screen exports (default) export { default as ContentDiscoverySettingsScreen } from './ContentDiscoverySettingsScreen'; export { default as AppearanceSettingsScreen } from './AppearanceSettingsScreen'; export { default as IntegrationsSettingsScreen } from './IntegrationsSettingsScreen'; export { default as PlaybackSettingsScreen } from './PlaybackSettingsScreen'; export { default as AboutSettingsScreen } from './AboutSettingsScreen'; export { default as DeveloperSettingsScreen } from './DeveloperSettingsScreen'; + +// Reusable content component exports (for inline use on tablets) +export { ContentDiscoverySettingsContent } from './ContentDiscoverySettingsScreen'; +export { AppearanceSettingsContent } from './AppearanceSettingsScreen'; +export { IntegrationsSettingsContent } from './IntegrationsSettingsScreen'; +export { PlaybackSettingsContent } from './PlaybackSettingsScreen'; +export { AboutSettingsContent, AboutFooter } from './AboutSettingsScreen'; + +// Shared UI component exports export { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';