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 && (
<>
<View style={styles.segmentCard}>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Hero Layout</Text>
<SegmentedControl
options={[
{ label: 'Legacy', value: 'legacy' },
{ label: 'Carousel', value: 'carousel' },
{ label: 'Apple TV', value: 'appletv' }
]}
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>
{!isTabletDevice && (
<View style={styles.segmentCard}>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Hero Layout</Text>
<SegmentedControl
options={[
{ label: 'Legacy', value: 'legacy' },
{ label: 'Carousel', value: 'carousel' },
{ label: 'Apple TV', value: 'appletv' }
]}
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>
)}
<View style={styles.segmentCard}>
<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 { 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<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 navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
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]);
const screenIsTablet = width >= 768;
return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -72,34 +205,7 @@ const AboutSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 40 }]}
>
<SettingsCard title="INFORMATION">
<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>
<AboutSettingsContent isTablet={screenIsTablet} />
<View style={{ height: 24 }} />
</ScrollView>
</View>

View file

@ -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<AppearanceSettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
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']) && (
<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 (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
@ -44,53 +115,7 @@ const AppearanceSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
>
{hasVisibleItems(['theme']) && (
<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>
)}
<AppearanceSettingsContent isTablet={screenIsTablet} />
</ScrollView>
</View>
);

View file

@ -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<ContentDiscoverySettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const { settings, updateSetting } = useSettings();
const insets = useSafeAreaInsets();
const config = useRealtimeConfig();
const [addonCount, setAddonCount] = useState<number>(0);
const [catalogCount, setCatalogCount] = useState<number>(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']) && (
<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 (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
@ -77,90 +192,7 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
>
{hasVisibleItems(['addons', 'debrid', 'plugins']) && (
<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>
)}
<ContentDiscoverySettingsContent isTablet={screenIsTablet} />
</ScrollView>
</View>
);

View file

@ -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<IntegrationsSettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const config = useRealtimeConfig();
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
const [openRouterKeySet, setOpenRouterKeySet] = useState<boolean>(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']) && (
<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 (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
@ -60,44 +125,7 @@ const IntegrationsSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
>
{hasVisibleItems(['mdblist', 'tmdb']) && (
<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>
)}
<IntegrationsSettingsContent isTablet={screenIsTablet} />
</ScrollView>
</View>
);

View file

@ -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<PlaybackSettingsContentProps> = ({ isTablet = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
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 (
<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 }]}
>
{hasVisibleItems(['video_player']) && (
<SettingsCard title="VIDEO PLAYER">
{isItemVisible('video_player') && (
<SettingItem
title="Video Player"
description={Platform.OS === 'ios'
? (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
/>
<>
{hasVisibleItems(['video_player']) && (
<SettingsCard title="VIDEO PLAYER" isTablet={isTablet}>
{isItemVisible('video_player') && (
<SettingItem
title="Video Player"
description={Platform.OS === 'ios'
? (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
isTablet={isTablet}
/>
)}
</SettingsCard>
)}
{hasVisibleItems(['show_trailers', 'enable_downloads']) && (
<SettingsCard title="MEDIA">
{isItemVisible('show_trailers') && (
<SettingItem
title="Show Trailers"
description="Display trailers in hero section"
icon="film"
renderControl={() => (
<CustomSwitch
value={settings?.showTrailers ?? true}
onValueChange={(value) => updateSetting('showTrailers', value)}
/>
)}
/>
)}
{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
/>
)}
</SettingsCard>
)}
{/* Audio & Subtitle Preferences */}
<SettingsCard title="AUDIO & SUBTITLES" isTablet={isTablet}>
<SettingItem
title="Preferred Audio Language"
description={getLanguageName(settings?.preferredAudioLanguage || 'en')}
icon="volume-2"
renderControl={() => <ChevronRight />}
onPress={openAudioLanguageSheet}
isTablet={isTablet}
/>
<SettingItem
title="Preferred Subtitle Language"
description={getLanguageName(settings?.preferredSubtitleLanguage || 'en')}
icon="type"
renderControl={() => <ChevronRight />}
onPress={openSubtitleLanguageSheet}
isTablet={isTablet}
/>
<SettingItem
title="Subtitle Source Priority"
description={getSourceLabel(settings?.subtitleSourcePreference || 'internal')}
icon="layers"
renderControl={() => <ChevronRight />}
onPress={openSubtitleSourceSheet}
isTablet={isTablet}
/>
<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
isTablet={isTablet}
/>
</SettingsCard>
{hasVisibleItems(['notifications']) && (
<SettingsCard title="NOTIFICATIONS">
{isItemVisible('notifications') && (
<SettingItem
title="Notifications"
description="Episode reminders"
icon="bell"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('NotificationSettings')}
isLast
/>
)}
</SettingsCard>
)}
</ScrollView>
{hasVisibleItems(['show_trailers', 'enable_downloads']) && (
<SettingsCard title="MEDIA" isTablet={isTablet}>
{isItemVisible('show_trailers') && (
<SettingItem
title="Show Trailers"
description="Display trailers in hero section"
icon="film"
renderControl={() => (
<CustomSwitch
value={settings?.showTrailers ?? true}
onValueChange={(value) => updateSetting('showTrailers', value)}
/>
)}
isTablet={isTablet}
/>
)}
{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 */}
<BottomSheetModal
@ -375,6 +384,32 @@ const PlaybackSettingsScreen: React.FC = () => {
})}
</BottomSheetScrollView>
</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 file

@ -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';