import { useFocusEffect } from '@react-navigation/native'; import React, { useCallback, useState, useEffect, useRef } from 'react'; import { useRealtimeConfig } from '../hooks/useRealtimeConfig'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, StatusBar, Platform, Dimensions, Linking, FlatList, } from 'react-native'; import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet'; import { useTranslation } from 'react-i18next'; import { mmkvStorage } from '../services/mmkvStorage'; import { useNavigation } from '@react-navigation/native'; 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 { 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 { fetchTotalDownloads } from '../services/githubReleaseService'; import * as WebBrowser from 'expo-web-browser'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { getDisplayedAppVersion } from '../utils/version'; import CustomAlert from '../components/CustomAlert'; import ScreenHeader from '../components/common/ScreenHeader'; import TraktIcon from '../components/icons/TraktIcon'; import { campaignService } from '../services/campaignService'; import { useScrollToTop } from '../contexts/ScrollToTopContext'; // 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'; import { useBottomSheetBackHandler } from '../hooks/useBottomSheetBackHandler'; import { LOCALES } from '../constants/locales'; const { width } = Dimensions.get('window'); const isTablet = width >= 768; // Settings categories for tablet sidebar // Settings categories moved inside component for translation // Tablet Sidebar Component interface SidebarProps { selectedCategory: string; onCategorySelect: (category: string) => void; currentTheme: any; categories: any[]; extraTopPadding?: number; } const Sidebar: React.FC = ({ selectedCategory, onCategorySelect, currentTheme, categories, extraTopPadding = 0 }) => { return ( Settings {categories.map((category) => ( onCategorySelect(category.id)} activeOpacity={0.6} > {category.title} ))} ); }; const SettingsScreen: React.FC = () => { const { t, i18n } = useTranslation(); const SETTINGS_CATEGORIES = [ { id: 'account', title: t('settings.account'), icon: 'user' }, { id: 'content', title: t('settings.content_discovery'), icon: 'compass' }, { id: 'appearance', title: t('settings.appearance'), icon: 'sliders' }, { id: 'integrations', title: t('settings.integrations'), icon: 'layers' }, { id: 'playback', title: t('settings.playback'), icon: 'play-circle' }, { id: 'backup', title: t('settings.backup_restore'), icon: 'archive' }, { id: 'updates', title: t('settings.updates'), icon: 'refresh-ccw' }, { id: 'about', title: t('settings.about'), icon: 'info' }, { id: 'developer', title: t('settings.developer'), icon: 'code' }, { id: 'cache', title: t('settings.cache'), icon: 'database' }, ]; const { settings, updateSetting } = useSettings(); const [hasUpdateBadge, setHasUpdateBadge] = useState(false); const languageSheetRef = useRef(null); const { onChange, onDismiss } = useBottomSheetBackHandler(); const insets = useSafeAreaInsets(); // Render backdrop for bottom sheet const renderBackdrop = useCallback( (props: any) => ( ), [] ); // CustomAlert state const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); const [alertMessage, setAlertMessage] = useState(''); const [alertActions, setAlertActions] = useState void; style?: object }>>([]); const openAlert = ( title: string, message: string, actions?: Array<{ label: string; onPress: () => void; style?: object }> ) => { setAlertTitle(title); setAlertMessage(message); setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]); setAlertVisible(true); }; useEffect(() => { if (Platform.OS !== 'android') return; let mounted = true; (async () => { try { const flag = await mmkvStorage.getItem('@update_badge_pending'); if (mounted) setHasUpdateBadge(flag === 'true'); } catch { } })(); return () => { mounted = false; }; }, []); const navigation = useNavigation>(); const { lastUpdate } = useCatalogContext(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { currentTheme } = useTheme(); // Tablet-specific state const [selectedCategory, setSelectedCategory] = useState('account'); // States for dynamic content const [mdblistKeySet, setMdblistKeySet] = useState(false); const [developerModeEnabled, setDeveloperModeEnabled] = useState(false); const [totalDownloads, setTotalDownloads] = useState(0); const [displayDownloads, setDisplayDownloads] = useState(null); // Use Realtime Config Hook const settingsConfig = useRealtimeConfig(); // Load developer mode state useEffect(() => { const loadDevModeState = async () => { try { const devModeEnabled = await mmkvStorage.getItem('developer_mode_enabled'); setDeveloperModeEnabled(devModeEnabled === 'true'); } catch (error) { if (__DEV__) console.error('Failed to load developer mode state:', error); } }; loadDevModeState(); }, []); // Scroll to top ref and handler const mobileScrollViewRef = useRef(null); const tabletScrollViewRef = useRef(null); const scrollToTop = useCallback(() => { mobileScrollViewRef.current?.scrollTo({ y: 0, animated: true }); tabletScrollViewRef.current?.scrollTo({ y: 0, animated: true }); }, []); useScrollToTop('Settings', scrollToTop); // Refresh Trakt auth status on focus useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { refreshAuthStatus(); }); return unsubscribe; }, [navigation, refreshAuthStatus]); const loadData = useCallback(async () => { try { // Check MDBList API key status const mdblistKey = await mmkvStorage.getItem('mdblist_api_key'); setMdblistKeySet(!!mdblistKey); // Check developer mode status const devModeEnabled = await mmkvStorage.getItem('developer_mode_enabled'); setDeveloperModeEnabled(devModeEnabled === 'true'); // 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); } }, []); useEffect(() => { loadData(); }, [loadData, lastUpdate]); useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { loadData(); }); return unsubscribe; }, [navigation, loadData]); // Poll GitHub downloads useEffect(() => { const shouldPoll = isTablet ? selectedCategory === 'about' : true; if (!shouldPoll) return; const pollInterval = setInterval(async () => { try { const downloads = await fetchTotalDownloads(); if (downloads !== null && downloads !== totalDownloads) { setTotalDownloads(downloads); } } catch (error) { if (__DEV__) console.error('Error polling downloads:', error); } }, 3600000); return () => clearInterval(pollInterval); }, [selectedCategory, totalDownloads]); // 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 handleClearMDBListCache = () => { openAlert( 'Clear MDBList Cache', 'Are you sure you want to clear all cached MDBList data? This cannot be undone.', [ { label: 'Cancel', onPress: () => { } }, { label: 'Clear', onPress: async () => { try { await mmkvStorage.removeItem('mdblist_cache'); openAlert('Success', 'MDBList cache has been cleared.'); } catch (error) { openAlert('Error', 'Could not clear MDBList cache.'); if (__DEV__) console.error('Error clearing MDBList cache:', error); } } } ] ); }; // Helper to check item visibility const isItemVisible = (itemId: string) => { if (!settingsConfig?.items) return true; const item = settingsConfig.items[itemId]; if (item && item.visible === false) return false; return true; }; // Filter categories based on conditions const visibleCategories = SETTINGS_CATEGORIES.filter(category => { if (settingsConfig?.categories?.[category.id]?.visible === false) return false; if (category.id === 'developer' && !__DEV__ && !developerModeEnabled) return false; if (category.id === 'cache' && !mdblistKeySet) return false; return true; }); // Render tablet category content using reusable components const renderCategoryContent = (categoryId: string) => { switch (categoryId) { case 'account': return ( {isItemVisible('trakt') && ( } renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} isLast={true} isTablet={isTablet} /> )} ); case 'content': return ; case 'appearance': return ( <> l.code === i18n.language)?.key}`)} icon="globe" renderControl={() => } onPress={() => languageSheetRef.current?.present()} isLast={true} isTablet={isTablet} /> ); case 'integrations': return ; case 'playback': return ; case 'about': return ; case 'developer': return (__DEV__ || developerModeEnabled) ? ( navigation.navigate('Onboarding')} renderControl={() => } isTablet={isTablet} /> navigation.navigate('PluginTester')} renderControl={() => } isTablet={isTablet} /> { try { await mmkvStorage.removeItem('hasCompletedOnboarding'); openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.'); } catch (error) { openAlert('Error', 'Failed to reset onboarding.'); } }} renderControl={() => } isTablet={isTablet} /> { await campaignService.resetCampaigns(); openAlert('Success', 'Campaign history reset. Restart app to see posters again.'); }} renderControl={() => } isTablet={isTablet} /> { openAlert( t('settings.clear_data'), t('settings.clear_data_desc'), [ { label: 'Cancel', onPress: () => { } }, { label: 'Clear', onPress: async () => { try { await mmkvStorage.clear(); openAlert('Success', 'All data cleared. Please restart the app.'); } catch (error) { openAlert('Error', 'Failed to clear data.'); } } } ] ); }} isLast={true} isTablet={isTablet} /> ) : null; case 'cache': return mdblistKeySet ? ( ) : null; case 'backup': return ( } onPress={() => navigation.navigate('Backup')} isLast={true} isTablet={isTablet} /> ); case 'updates': return ( } badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined} onPress={async () => { if (Platform.OS === 'android') { try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } setHasUpdateBadge(false); } navigation.navigate('Update'); }} isLast={true} isTablet={isTablet} /> ); default: return null; } }; // Keep headers below floating top navigator on tablets const tabletNavOffset = isTablet ? 64 : 0; // TABLET LAYOUT if (isTablet) { return ( {renderCategoryContent(selectedCategory)} {selectedCategory === 'about' && ( )} setAlertVisible(false)} /> {t('settings.select_language')} languageSheetRef.current?.dismiss()}> { LOCALES.sort((a,b) => a.key.localeCompare(b.key)).map(l => { i18n.changeLanguage(l.code); languageSheetRef.current?.dismiss(); }} > {t(`settings.${l.key}`)} {i18n.language === l.code && ( )} ) } ); } // MOBILE LAYOUT - Simplified navigation hub return ( {/* Account */} {(settingsConfig?.categories?.['account']?.visible !== false) && isItemVisible('trakt') && ( {isItemVisible('trakt') && ( } renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} isLast /> )} )} {/* General Settings */} {( (settingsConfig?.categories?.['content']?.visible !== false) || (settingsConfig?.categories?.['appearance']?.visible !== false) || (settingsConfig?.categories?.['integrations']?.visible !== false) || (settingsConfig?.categories?.['playback']?.visible !== false) ) && ( l.code === i18n.language)?.key}`) } icon="globe" renderControl={() => } onPress={() => languageSheetRef.current?.present()} /> {(settingsConfig?.categories?.['content']?.visible !== false) && ( } onPress={() => navigation.navigate('ContentDiscoverySettings')} /> )} {(settingsConfig?.categories?.['appearance']?.visible !== false) && ( } onPress={() => navigation.navigate('AppearanceSettings')} /> )} {(settingsConfig?.categories?.['integrations']?.visible !== false) && ( } onPress={() => navigation.navigate('IntegrationsSettings')} /> )} {(settingsConfig?.categories?.['playback']?.visible !== false) && ( } onPress={() => navigation.navigate('PlaybackSettings')} isLast /> )} )} {/* Data */} {( (settingsConfig?.categories?.['backup']?.visible !== false) || (settingsConfig?.categories?.['updates']?.visible !== false) ) && ( {(settingsConfig?.categories?.['backup']?.visible !== false) && ( } onPress={() => navigation.navigate('Backup')} /> )} {(settingsConfig?.categories?.['updates']?.visible !== false) && ( } onPress={async () => { if (Platform.OS === 'android') { try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } setHasUpdateBadge(false); } navigation.navigate('Update'); }} isLast /> )} )} {/* Cache - only if MDBList is set */} {mdblistKeySet && ( )} {/* About */} } onPress={() => navigation.navigate('AboutSettings')} isLast /> {/* Developer - visible in DEV mode or when developer mode is enabled */} {(__DEV__ || developerModeEnabled) && ( } onPress={() => navigation.navigate('DeveloperSettings')} isLast /> )} {/* Downloads Counter */} {settingsConfig?.items?.['downloads_counter']?.visible !== false && displayDownloads !== null && ( {displayDownloads.toLocaleString()} {t('settings.downloads_counter')} )} {/* Support & Community Buttons */} 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 */} {t('settings.made_with_love')} setAlertVisible(false)} /> {t('settings.select_language')} languageSheetRef.current?.dismiss()}> { LOCALES.sort((a,b) => a.key.localeCompare(b.key)).map(l => { i18n.changeLanguage(l.code); languageSheetRef.current?.dismiss(); }} > {t(`settings.${l.key}`)} {i18n.language === l.code && ( )} ) } ); }; const styles = StyleSheet.create({ container: { flex: 1, }, actionSheetContent: { flex: 1, }, bottomSheetHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.1)', }, bottomSheetTitle: { fontSize: 18, fontWeight: '600', }, bottomSheetContent: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 24, }, languageOption: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 14, paddingHorizontal: 16, borderRadius: 8, marginBottom: 8, }, languageText: { fontSize: 16, }, // Mobile styles contentContainer: { flex: 1, zIndex: 1, width: '100%', }, scrollView: { flex: 1, width: '100%', }, scrollContent: { flexGrow: 1, width: '100%', paddingTop: 8, paddingBottom: 32, }, // Tablet-specific styles tabletContainer: { flex: 1, flexDirection: 'row', }, sidebar: { width: 280, borderRightWidth: 1, }, sidebarHeader: { paddingHorizontal: 24, paddingBottom: 20, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 24 : 48, borderBottomWidth: 1, }, sidebarTitle: { fontSize: 42, fontWeight: '700', letterSpacing: -0.3, }, sidebarContent: { flex: 1, paddingTop: 12, paddingBottom: 24, }, sidebarItem: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, marginHorizontal: 12, marginVertical: 2, borderRadius: 10, }, sidebarItemActive: { borderRadius: 10, }, sidebarItemIconContainer: { width: 32, height: 32, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, sidebarItemText: { fontSize: 15, marginLeft: 12, }, tabletContent: { flex: 1, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 24 : 48, }, tabletScrollView: { flex: 1, paddingHorizontal: 40, }, tabletScrollContent: { paddingTop: 8, paddingBottom: 40, }, // Footer and social styles footer: { alignItems: 'center', justifyContent: 'center', marginTop: 0, marginBottom: 48, }, footerText: { fontSize: 13, opacity: 0.5, letterSpacing: 0.2, }, discordContainer: { marginTop: 12, marginBottom: 24, alignItems: 'center', }, discordButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, paddingHorizontal: 18, borderRadius: 10, maxWidth: 200, }, discordButtonContent: { flexDirection: 'row', alignItems: 'center', }, discordLogo: { width: 18, height: 18, marginRight: 10, }, discordButtonText: { fontSize: 14, fontWeight: '600', }, kofiImage: { height: 34, width: 155, }, downloadsContainer: { marginTop: 32, marginBottom: 16, alignItems: 'center', }, downloadsNumber: { fontSize: 36, fontWeight: '800', letterSpacing: 0.5, marginBottom: 6, }, downloadsLabel: { fontSize: 11, fontWeight: '600', opacity: 0.5, letterSpacing: 1.5, textTransform: 'uppercase', }, monkeyContainer: { alignItems: 'center', justifyContent: 'center', marginTop: 0, marginBottom: 16, }, monkeyAnimation: { width: 180, height: 180, }, brandLogoContainer: { alignItems: 'center', justifyContent: 'center', marginTop: 0, marginBottom: 16, opacity: 0.8, }, brandLogo: { width: 120, height: 40, }, }); export default SettingsScreen;