refactored settingscreen

This commit is contained in:
tapframe 2025-12-31 03:15:37 +05:30
parent 3285ecbe04
commit bf75cca438
8 changed files with 665 additions and 1142 deletions

View file

@ -339,19 +339,21 @@ const HomeScreenSettings: React.FC = () => {
{settings.showHeroSection && ( {settings.showHeroSection && (
<> <>
<View style={styles.segmentCard}> {!isTabletDevice && (
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Hero Layout</Text> <View style={styles.segmentCard}>
<SegmentedControl <Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Hero Layout</Text>
options={[ <SegmentedControl
{ label: 'Legacy', value: 'legacy' }, options={[
{ label: 'Carousel', value: 'carousel' }, { label: 'Legacy', value: 'legacy' },
{ label: 'Apple TV', value: 'appletv' } { label: 'Carousel', value: 'carousel' },
]} { label: 'Apple TV', value: 'appletv' }
value={settings.heroStyle} ]}
onChange={(val) => handleUpdateSetting('heroStyle', val as any)} value={settings.heroStyle}
/> onChange={(val) => handleUpdateSetting('heroStyle', val as any)}
<Text style={[styles.segmentHint, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Full-width banner, swipeable cards, or Apple TV style</Text> />
</View> <Text style={[styles.segmentHint, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Full-width banner, swipeable cards, or Apple TV style</Text>
</View>
)}
<View style={styles.segmentCard}> <View style={styles.segmentCard}>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Featured Source</Text> <Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Featured Source</Text>

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -14,53 +14,186 @@ import { getDisplayedAppVersion } from '../../utils/version';
import ScreenHeader from '../../components/common/ScreenHeader'; import ScreenHeader from '../../components/common/ScreenHeader';
import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents'; 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<AboutSettingsContentProps> = ({
isTablet = false,
displayDownloads: externalDisplayDownloads
}) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const [internalDisplayDownloads, setInternalDisplayDownloads] = useState<number | null>(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 (
<>
<SettingsCard title="INFORMATION" isTablet={isTablet}>
<SettingItem
title="Privacy Policy"
icon="lock"
onPress={() => Linking.openURL('https://tapframe.github.io/NuvioStreaming/#privacy-policy')}
renderControl={() => <ChevronRight />}
isTablet={isTablet}
/>
<SettingItem
title="Report Issue"
icon="alert-triangle"
onPress={() => Sentry.showFeedbackWidget()}
renderControl={() => <ChevronRight />}
isTablet={isTablet}
/>
<SettingItem
title="Version"
description={getDisplayedAppVersion()}
icon="info"
isTablet={isTablet}
/>
<SettingItem
title="Contributors"
description="View all contributors"
icon="users"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('Contributors')}
isLast
isTablet={isTablet}
/>
</SettingsCard>
</>
);
};
/**
* Reusable AboutFooter component - Downloads counter, social links, branding
*/
export const AboutFooter: React.FC<{ displayDownloads: number | null }> = ({ displayDownloads }) => {
const { currentTheme } = useTheme();
return (
<>
{displayDownloads !== null && (
<View style={styles.downloadsContainer}>
<Text style={[styles.downloadsNumber, { color: currentTheme.colors.primary }]}>
{displayDownloads.toLocaleString()}
</Text>
<Text style={[styles.downloadsLabel, { color: currentTheme.colors.mediumEmphasis }]}>
downloads and counting
</Text>
</View>
)}
<View style={styles.communityContainer}>
<TouchableOpacity
style={styles.supportButton}
onPress={() => WebBrowser.openBrowserAsync('https://ko-fi.com/tapframe', {
presentationStyle: Platform.OS === 'ios' ? WebBrowser.WebBrowserPresentationStyle.FORM_SHEET : WebBrowser.WebBrowserPresentationStyle.FORM_SHEET
})}
activeOpacity={0.7}
>
<FastImage
source={require('../../../assets/support_me_on_kofi_red.png')}
style={styles.kofiImage}
resizeMode={FastImage.resizeMode.contain}
/>
</TouchableOpacity>
<View style={styles.socialRow}>
<TouchableOpacity
style={[styles.socialButton, { backgroundColor: currentTheme.colors.elevation1 }]}
onPress={() => Linking.openURL('https://discord.gg/KVgDTjhA4H')}
activeOpacity={0.7}
>
<View style={styles.socialButtonContent}>
<FastImage
source={{ uri: 'https://pngimg.com/uploads/discord/discord_PNG3.png' }}
style={styles.socialLogo}
resizeMode={FastImage.resizeMode.contain}
/>
<Text style={[styles.socialButtonText, { color: currentTheme.colors.highEmphasis }]}>
Discord
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
style={[styles.socialButton, { backgroundColor: '#FF4500' + '15' }]}
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
activeOpacity={0.7}
>
<View style={styles.socialButtonContent}>
<FastImage
source={{ uri: 'https://www.iconpacks.net/icons/2/free-reddit-logo-icon-2436-thumb.png' }}
style={styles.socialLogo}
resizeMode={FastImage.resizeMode.contain}
/>
<Text style={[styles.socialButtonText, { color: '#FF4500' }]}>
Reddit
</Text>
</View>
</TouchableOpacity>
</View>
</View>
{/* Monkey Animation */}
<View style={styles.monkeyContainer}>
<LottieView
source={require('../../assets/lottie/monito.json')}
autoPlay
loop
style={styles.monkeyAnimation}
resizeMode="contain"
/>
</View>
<View style={styles.brandLogoContainer}>
<FastImage
source={require('../../../assets/nuviotext.png')}
style={styles.brandLogo}
resizeMode={FastImage.resizeMode.contain}
/>
</View>
<View style={styles.footer}>
<Text style={[styles.footerText, { color: currentTheme.colors.mediumEmphasis }]}>
Made with by Tapframe and Friends
</Text>
</View>
</>
);
};
/**
* AboutSettingsScreen - Wrapper for mobile navigation
*/
const AboutSettingsScreen: React.FC = () => { const AboutSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const screenIsTablet = width >= 768;
const [totalDownloads, setTotalDownloads] = useState<number | null>(null);
const [displayDownloads, setDisplayDownloads] = useState<number | null>(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]);
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -72,34 +205,7 @@ const AboutSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 40 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 40 }]}
> >
<SettingsCard title="INFORMATION"> <AboutSettingsContent isTablet={screenIsTablet} />
<SettingItem
title="Privacy Policy"
icon="lock"
onPress={() => Linking.openURL('https://tapframe.github.io/NuvioStreaming/#privacy-policy')}
renderControl={() => <ChevronRight />}
/>
<SettingItem
title="Report Issue"
icon="alert-triangle"
onPress={() => Sentry.showFeedbackWidget()}
renderControl={() => <ChevronRight />}
/>
<SettingItem
title="Version"
description={getDisplayedAppVersion()}
icon="info"
/>
<SettingItem
title="Contributors"
description="View all contributors"
icon="users"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('Contributors')}
isLast
/>
</SettingsCard>
<View style={{ height: 24 }} /> <View style={{ height: 24 }} />
</ScrollView> </ScrollView>
</View> </View>

View file

@ -1,6 +1,6 @@
import React, { useState, useCallback } from 'react'; import React from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Dimensions } from 'react-native'; import { View, StyleSheet, ScrollView, StatusBar, Dimensions } from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
@ -11,13 +11,19 @@ import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './Setting
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
const { width } = Dimensions.get('window'); 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<AppearanceSettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const insets = useSafeAreaInsets();
const config = useRealtimeConfig(); const config = useRealtimeConfig();
const isItemVisible = (itemId: string) => { const isItemVisible = (itemId: string) => {
@ -34,6 +40,71 @@ const AppearanceSettingsScreen: React.FC = () => {
}); });
}; };
return (
<>
{hasVisibleItems(['theme']) && (
<SettingsCard title="THEME" isTablet={isTablet}>
{isItemVisible('theme') && (
<SettingItem
title="Theme"
description={currentTheme.name}
icon="sliders"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ThemeSettings')}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['episode_layout', 'streams_backdrop']) && (
<SettingsCard title="LAYOUT" isTablet={isTablet}>
{isItemVisible('episode_layout') && (
<SettingItem
title="Episode Layout"
description={settings?.episodeLayoutStyle === 'horizontal' ? 'Horizontal' : 'Vertical'}
icon="grid"
renderControl={() => (
<CustomSwitch
value={settings?.episodeLayoutStyle === 'horizontal'}
onValueChange={(value) => updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')}
/>
)}
isLast={isTablet || !isItemVisible('streams_backdrop')}
isTablet={isTablet}
/>
)}
{!isTablet && isItemVisible('streams_backdrop') && (
<SettingItem
title="Streams Backdrop"
description="Show blurred backdrop on mobile streams"
icon="image"
renderControl={() => (
<CustomSwitch
value={settings?.enableStreamsBackdrop ?? true}
onValueChange={(value) => updateSetting('enableStreamsBackdrop', value)}
/>
)}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
</>
);
};
/**
* AppearanceSettingsScreen - Wrapper for mobile navigation
*/
const AppearanceSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const screenIsTablet = width >= 768;
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
@ -44,53 +115,7 @@ const AppearanceSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
{hasVisibleItems(['theme']) && ( <AppearanceSettingsContent isTablet={screenIsTablet} />
<SettingsCard title="THEME">
{isItemVisible('theme') && (
<SettingItem
title="Theme"
description={currentTheme.name}
icon="sliders"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ThemeSettings')}
isLast
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['episode_layout', 'streams_backdrop']) && (
<SettingsCard title="LAYOUT">
{isItemVisible('episode_layout') && (
<SettingItem
title="Episode Layout"
description={settings?.episodeLayoutStyle === 'horizontal' ? 'Horizontal' : 'Vertical'}
icon="grid"
renderControl={() => (
<CustomSwitch
value={settings?.episodeLayoutStyle === 'horizontal'}
onValueChange={(value) => updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')}
/>
)}
isLast={isTablet || !isItemVisible('streams_backdrop')}
/>
)}
{!isTablet && isItemVisible('streams_backdrop') && (
<SettingItem
title="Streams Backdrop"
description="Show blurred backdrop on mobile streams"
icon="image"
renderControl={() => (
<CustomSwitch
value={settings?.enableStreamsBackdrop ?? true}
onValueChange={(value) => updateSetting('enableStreamsBackdrop', value)}
/>
)}
isLast
/>
)}
</SettingsCard>
)}
</ScrollView> </ScrollView>
</View> </View>
); );

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; 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 { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; 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 { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; 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<ContentDiscoverySettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const insets = useSafeAreaInsets(); const config = useRealtimeConfig();
const [addonCount, setAddonCount] = useState<number>(0); const [addonCount, setAddonCount] = useState<number>(0);
const [catalogCount, setCatalogCount] = useState<number>(0); const [catalogCount, setCatalogCount] = useState<number>(0);
const config = useRealtimeConfig();
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
try { try {
@ -67,6 +76,112 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
return itemIds.some(id => isItemVisible(id)); return itemIds.some(id => isItemVisible(id));
}; };
return (
<>
{hasVisibleItems(['addons', 'debrid', 'plugins']) && (
<SettingsCard title="SOURCES" isTablet={isTablet}>
{isItemVisible('addons') && (
<SettingItem
title="Addons"
description={`${addonCount} installed`}
icon="layers"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('Addons')}
isTablet={isTablet}
/>
)}
{isItemVisible('debrid') && (
<SettingItem
title="Debrid Integration"
description="Connect Torbox for premium streams"
icon="link"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('DebridIntegration')}
isTablet={isTablet}
/>
)}
{isItemVisible('plugins') && (
<SettingItem
title="Plugins"
description="Manage plugins and repositories"
customIcon={<PluginIcon size={isTablet ? 22 : 18} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ScraperSettings')}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['catalogs', 'home_screen', 'continue_watching']) && (
<SettingsCard title="CATALOGS" isTablet={isTablet}>
{isItemVisible('catalogs') && (
<SettingItem
title="Catalogs"
description={`${catalogCount} active`}
icon="list"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('CatalogSettings')}
isTablet={isTablet}
/>
)}
{isItemVisible('home_screen') && (
<SettingItem
title="Home Screen"
description="Layout and content"
icon="home"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('HomeScreenSettings')}
isTablet={isTablet}
/>
)}
{isItemVisible('continue_watching') && (
<SettingItem
title="Continue Watching"
description="Cache and playback behavior"
icon="play-circle"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ContinueWatchingSettings')}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['show_discover']) && (
<SettingsCard title="DISCOVERY" isTablet={isTablet}>
{isItemVisible('show_discover') && (
<SettingItem
title="Show Discover Section"
description="Display discover content in Search"
icon="compass"
renderControl={() => (
<CustomSwitch
value={settings?.showDiscover ?? true}
onValueChange={(value) => updateSetting('showDiscover', value)}
/>
)}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
</>
);
};
/**
* ContentDiscoverySettingsScreen - Wrapper for mobile navigation
*/
const ContentDiscoverySettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const screenIsTablet = width >= 768;
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
@ -77,90 +192,7 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
{hasVisibleItems(['addons', 'debrid', 'plugins']) && ( <ContentDiscoverySettingsContent isTablet={screenIsTablet} />
<SettingsCard title="SOURCES">
{isItemVisible('addons') && (
<SettingItem
title="Addons"
description={`${addonCount} installed`}
icon="layers"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('Addons')}
/>
)}
{isItemVisible('debrid') && (
<SettingItem
title="Debrid Integration"
description="Connect Torbox for premium streams"
icon="link"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('DebridIntegration')}
/>
)}
{isItemVisible('plugins') && (
<SettingItem
title="Plugins"
description="Manage plugins and repositories"
customIcon={<PluginIcon size={18} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ScraperSettings')}
isLast
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['catalogs', 'home_screen', 'continue_watching']) && (
<SettingsCard title="CATALOGS">
{isItemVisible('catalogs') && (
<SettingItem
title="Catalogs"
description={`${catalogCount} active`}
icon="list"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('CatalogSettings')}
/>
)}
{isItemVisible('home_screen') && (
<SettingItem
title="Home Screen"
description="Layout and content"
icon="home"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('HomeScreenSettings')}
/>
)}
{isItemVisible('continue_watching') && (
<SettingItem
title="Continue Watching"
description="Cache and playback behavior"
icon="play-circle"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ContinueWatchingSettings')}
isLast
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['show_discover']) && (
<SettingsCard title="DISCOVERY">
{isItemVisible('show_discover') && (
<SettingItem
title="Show Discover Section"
description="Display discover content in Search"
icon="compass"
renderControl={() => (
<CustomSwitch
value={settings?.showDiscover ?? true}
onValueChange={(value) => updateSetting('showDiscover', value)}
/>
)}
isLast
/>
)}
</SettingsCard>
)}
</ScrollView> </ScrollView>
</View> </View>
); );

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { View, StyleSheet, ScrollView, StatusBar } from 'react-native'; import { View, StyleSheet, ScrollView, StatusBar, Dimensions } from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; 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 { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents';
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig'; 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<IntegrationsSettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const insets = useSafeAreaInsets(); const config = useRealtimeConfig();
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false); const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
const [openRouterKeySet, setOpenRouterKeySet] = useState<boolean>(false); const [openRouterKeySet, setOpenRouterKeySet] = useState<boolean>(false);
const config = useRealtimeConfig();
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
try { try {
@ -50,6 +59,62 @@ const IntegrationsSettingsScreen: React.FC = () => {
return itemIds.some(id => isItemVisible(id)); return itemIds.some(id => isItemVisible(id));
}; };
return (
<>
{hasVisibleItems(['mdblist', 'tmdb']) && (
<SettingsCard title="METADATA" isTablet={isTablet}>
{isItemVisible('mdblist') && (
<SettingItem
title="MDBList"
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
customIcon={<MDBListIcon size={isTablet ? 22 : 18} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('MDBListSettings')}
isTablet={isTablet}
/>
)}
{isItemVisible('tmdb') && (
<SettingItem
title="TMDB"
description="Metadata & logo source provider"
customIcon={<TMDBIcon size={isTablet ? 22 : 18} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('TMDBSettings')}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['openrouter']) && (
<SettingsCard title="AI ASSISTANT" isTablet={isTablet}>
{isItemVisible('openrouter') && (
<SettingItem
title="OpenRouter API"
description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"}
icon="cpu"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('AISettings')}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
</>
);
};
/**
* IntegrationsSettingsScreen - Wrapper for mobile navigation
*/
const IntegrationsSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const screenIsTablet = width >= 768;
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
@ -60,44 +125,7 @@ const IntegrationsSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
{hasVisibleItems(['mdblist', 'tmdb']) && ( <IntegrationsSettingsContent isTablet={screenIsTablet} />
<SettingsCard title="METADATA">
{isItemVisible('mdblist') && (
<SettingItem
title="MDBList"
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
customIcon={<MDBListIcon size={18} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('MDBListSettings')}
/>
)}
{isItemVisible('tmdb') && (
<SettingItem
title="TMDB"
description="Metadata & logo source provider"
customIcon={<TMDBIcon size={18} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('TMDBSettings')}
isLast
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['openrouter']) && (
<SettingsCard title="AI ASSISTANT">
{isItemVisible('openrouter') && (
<SettingItem
title="OpenRouter API"
description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"}
icon="cpu"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('AISettings')}
isLast
/>
)}
</SettingsCard>
)}
</ScrollView> </ScrollView>
</View> </View>
); );

View file

@ -1,5 +1,5 @@
import React, { useState, useCallback, useMemo, useRef } from 'react'; 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 { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -12,6 +12,8 @@ import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
const { width } = Dimensions.get('window');
// Available languages for audio/subtitle selection // Available languages for audio/subtitle selection
const AVAILABLE_LANGUAGES = [ const AVAILABLE_LANGUAGES = [
{ code: 'en', name: 'English' }, { code: 'en', name: 'English' },
@ -54,11 +56,19 @@ const SUBTITLE_SOURCE_OPTIONS = [
{ value: 'any', label: 'Any Available', description: 'Use first available subtitle track' }, { 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<PlaybackSettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const insets = useSafeAreaInsets();
const config = useRealtimeConfig(); const config = useRealtimeConfig();
// Bottom sheet refs // Bottom sheet refs
@ -139,117 +149,116 @@ const PlaybackSettingsScreen: React.FC = () => {
}; };
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <>
<StatusBar barStyle="light-content" /> {hasVisibleItems(['video_player']) && (
<ScreenHeader title="Playback" showBackButton onBackPress={() => navigation.goBack()} /> <SettingsCard title="VIDEO PLAYER" isTablet={isTablet}>
{isItemVisible('video_player') && (
<ScrollView <SettingItem
style={styles.scrollView} title="Video Player"
showsVerticalScrollIndicator={false} description={Platform.OS === 'ios'
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} ? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in')
> : (settings?.useExternalPlayer ? 'External' : 'Built-in')
{hasVisibleItems(['video_player']) && ( }
<SettingsCard title="VIDEO PLAYER"> icon="play-circle"
{isItemVisible('video_player') && ( renderControl={() => <ChevronRight />}
<SettingItem onPress={() => navigation.navigate('PlayerSettings')}
title="Video Player" isLast
description={Platform.OS === 'ios' isTablet={isTablet}
? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in') />
: (settings?.useExternalPlayer ? 'External' : 'Built-in') )}
}
icon="play-circle"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('PlayerSettings')}
isLast
/>
)}
</SettingsCard>
)}
{/* Audio & Subtitle Preferences */}
<SettingsCard title="AUDIO & SUBTITLES">
<SettingItem
title="Preferred Audio Language"
description={getLanguageName(settings?.preferredAudioLanguage || 'en')}
icon="volume-2"
renderControl={() => <ChevronRight />}
onPress={openAudioLanguageSheet}
/>
<SettingItem
title="Preferred Subtitle Language"
description={getLanguageName(settings?.preferredSubtitleLanguage || 'en')}
icon="type"
renderControl={() => <ChevronRight />}
onPress={openSubtitleLanguageSheet}
/>
<SettingItem
title="Subtitle Source Priority"
description={getSourceLabel(settings?.subtitleSourcePreference || 'internal')}
icon="layers"
renderControl={() => <ChevronRight />}
onPress={openSubtitleSourceSheet}
/>
<SettingItem
title="Auto-Select Subtitles"
description="Automatically select subtitles matching your preferences"
icon="zap"
renderControl={() => (
<CustomSwitch
value={settings?.enableSubtitleAutoSelect ?? true}
onValueChange={(value) => updateSetting('enableSubtitleAutoSelect', value)}
/>
)}
isLast
/>
</SettingsCard> </SettingsCard>
)}
{hasVisibleItems(['show_trailers', 'enable_downloads']) && ( {/* Audio & Subtitle Preferences */}
<SettingsCard title="MEDIA"> <SettingsCard title="AUDIO & SUBTITLES" isTablet={isTablet}>
{isItemVisible('show_trailers') && ( <SettingItem
<SettingItem title="Preferred Audio Language"
title="Show Trailers" description={getLanguageName(settings?.preferredAudioLanguage || 'en')}
description="Display trailers in hero section" icon="volume-2"
icon="film" renderControl={() => <ChevronRight />}
renderControl={() => ( onPress={openAudioLanguageSheet}
<CustomSwitch isTablet={isTablet}
value={settings?.showTrailers ?? true} />
onValueChange={(value) => updateSetting('showTrailers', value)} <SettingItem
/> title="Preferred Subtitle Language"
)} description={getLanguageName(settings?.preferredSubtitleLanguage || 'en')}
/> icon="type"
)} renderControl={() => <ChevronRight />}
{isItemVisible('enable_downloads') && ( onPress={openSubtitleLanguageSheet}
<SettingItem isTablet={isTablet}
title="Enable Downloads (Beta)" />
description="Show Downloads tab and enable saving streams" <SettingItem
icon="download" title="Subtitle Source Priority"
renderControl={() => ( description={getSourceLabel(settings?.subtitleSourcePreference || 'internal')}
<CustomSwitch icon="layers"
value={settings?.enableDownloads ?? false} renderControl={() => <ChevronRight />}
onValueChange={(value) => updateSetting('enableDownloads', value)} onPress={openSubtitleSourceSheet}
/> isTablet={isTablet}
)} />
isLast <SettingItem
/> title="Auto-Select Subtitles"
)} description="Automatically select subtitles matching your preferences"
</SettingsCard> icon="zap"
)} renderControl={() => (
<CustomSwitch
value={settings?.enableSubtitleAutoSelect ?? true}
onValueChange={(value) => updateSetting('enableSubtitleAutoSelect', value)}
/>
)}
isLast
isTablet={isTablet}
/>
</SettingsCard>
{hasVisibleItems(['notifications']) && ( {hasVisibleItems(['show_trailers', 'enable_downloads']) && (
<SettingsCard title="NOTIFICATIONS"> <SettingsCard title="MEDIA" isTablet={isTablet}>
{isItemVisible('notifications') && ( {isItemVisible('show_trailers') && (
<SettingItem <SettingItem
title="Notifications" title="Show Trailers"
description="Episode reminders" description="Display trailers in hero section"
icon="bell" icon="film"
renderControl={() => <ChevronRight />} renderControl={() => (
onPress={() => navigation.navigate('NotificationSettings')} <CustomSwitch
isLast value={settings?.showTrailers ?? true}
/> onValueChange={(value) => updateSetting('showTrailers', value)}
)} />
</SettingsCard> )}
)} isTablet={isTablet}
</ScrollView> />
)}
{isItemVisible('enable_downloads') && (
<SettingItem
title="Enable Downloads (Beta)"
description="Show Downloads tab and enable saving streams"
icon="download"
renderControl={() => (
<CustomSwitch
value={settings?.enableDownloads ?? false}
onValueChange={(value) => updateSetting('enableDownloads', value)}
/>
)}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['notifications']) && (
<SettingsCard title="NOTIFICATIONS" isTablet={isTablet}>
{isItemVisible('notifications') && (
<SettingItem
title="Notifications"
description="Episode reminders"
icon="bell"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('NotificationSettings')}
isLast
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{/* Audio Language Bottom Sheet */} {/* Audio Language Bottom Sheet */}
<BottomSheetModal <BottomSheetModal
@ -375,6 +384,32 @@ const PlaybackSettingsScreen: React.FC = () => {
})} })}
</BottomSheetScrollView> </BottomSheetScrollView>
</BottomSheetModal> </BottomSheetModal>
</>
);
};
/**
* PlaybackSettingsScreen - Wrapper for mobile navigation
* Uses PlaybackSettingsContent internally
*/
const PlaybackSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const screenIsTablet = width >= 768;
return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
<ScreenHeader title="Playback" showBackButton onBackPress={() => navigation.goBack()} />
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
>
<PlaybackSettingsContent isTablet={screenIsTablet} />
</ScrollView>
</View> </View>
); );
}; };

View file

@ -1,7 +1,17 @@
// Screen exports (default)
export { default as ContentDiscoverySettingsScreen } from './ContentDiscoverySettingsScreen'; export { default as ContentDiscoverySettingsScreen } from './ContentDiscoverySettingsScreen';
export { default as AppearanceSettingsScreen } from './AppearanceSettingsScreen'; export { default as AppearanceSettingsScreen } from './AppearanceSettingsScreen';
export { default as IntegrationsSettingsScreen } from './IntegrationsSettingsScreen'; export { default as IntegrationsSettingsScreen } from './IntegrationsSettingsScreen';
export { default as PlaybackSettingsScreen } from './PlaybackSettingsScreen'; export { default as PlaybackSettingsScreen } from './PlaybackSettingsScreen';
export { default as AboutSettingsScreen } from './AboutSettingsScreen'; export { default as AboutSettingsScreen } from './AboutSettingsScreen';
export { default as DeveloperSettingsScreen } from './DeveloperSettingsScreen'; 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'; export { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';