more sdui control over settinsgcreen.

This commit is contained in:
tapframe 2025-12-30 02:49:10 +05:30
parent 44abb9f635
commit 4173786b12
7 changed files with 692 additions and 430 deletions

View file

@ -0,0 +1,30 @@
import { useState, useCallback } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import { configService, SettingsConfig } from '../services/configService';
export const useRealtimeConfig = () => {
const [config, setConfig] = useState<SettingsConfig | null>(null);
const loadConfig = useCallback(async () => {
try {
const fetchedConfig = await configService.getSettingsConfig();
// Deep compare to avoid unnecessary re-renders
setConfig(prev => {
const prevStr = JSON.stringify(prev);
const newStr = JSON.stringify(fetchedConfig);
return prevStr === newStr ? prev : fetchedConfig;
});
} catch (error) {
if (__DEV__) console.warn('Config fetch failed', error);
}
}, []);
useFocusEffect(
useCallback(() => {
loadConfig(); // Fetch on focus (will use memory cache if available)
}, [loadConfig])
);
return config;
};

View file

@ -1,4 +1,8 @@
import { useFocusEffect } from '@react-navigation/native';
import React, { useCallback, useState, useEffect, useRef } from 'react'; import React, { useCallback, useState, useEffect, useRef } from 'react';
import { useRealtimeConfig } from '../hooks/useRealtimeConfig';
import { import {
View, View,
Text, Text,
@ -316,11 +320,13 @@ const SettingsScreen: React.FC = () => {
const [catalogCount, setCatalogCount] = useState<number>(0); const [catalogCount, setCatalogCount] = useState<number>(0);
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 [initialLoadComplete, setInitialLoadComplete] = useState<boolean>(false); const [totalDownloads, setTotalDownloads] = useState<number>(0);
const [totalDownloads, setTotalDownloads] = useState<number | null>(null);
const [displayDownloads, setDisplayDownloads] = useState<number | null>(null); const [displayDownloads, setDisplayDownloads] = useState<number | null>(null);
const [isCountingUp, setIsCountingUp] = useState<boolean>(false); const [isCountingUp, setIsCountingUp] = useState<boolean>(false);
// Use Realtime Config Hook
const settingsConfig = useRealtimeConfig();
// Scroll to top ref and handler // Scroll to top ref and handler
const mobileScrollViewRef = useRef<ScrollView>(null); const mobileScrollViewRef = useRef<ScrollView>(null);
const tabletScrollViewRef = useRef<ScrollView>(null); const tabletScrollViewRef = useRef<ScrollView>(null);
@ -354,7 +360,6 @@ const SettingsScreen: React.FC = () => {
// Load addon count and get their catalogs // Load addon count and get their catalogs
const addons = await stremioService.getInstalledAddonsAsync(); const addons = await stremioService.getInstalledAddonsAsync();
setAddonCount(addons.length); setAddonCount(addons.length);
setInitialLoadComplete(true);
// Count total available catalogs // Count total available catalogs
let totalCatalogs = 0; let totalCatalogs = 0;
@ -525,8 +530,17 @@ const SettingsScreen: React.FC = () => {
/> />
); );
// 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 // Filter categories based on conditions
const visibleCategories = SETTINGS_CATEGORIES.filter(category => { const visibleCategories = SETTINGS_CATEGORIES.filter(category => {
if (settingsConfig?.categories?.[category.id]?.visible === false) return false;
if (category.id === 'developer' && !__DEV__) return false; if (category.id === 'developer' && !__DEV__) return false;
if (category.id === 'cache' && !mdblistKeySet) return false; if (category.id === 'cache' && !mdblistKeySet) return false;
return true; return true;
@ -539,110 +553,130 @@ const SettingsScreen: React.FC = () => {
case 'account': case 'account':
return ( return (
<SettingsCard title="ACCOUNT" isTablet={isTablet}> <SettingsCard title="ACCOUNT" isTablet={isTablet}>
<SettingItem {isItemVisible('trakt') && (
title="Trakt" <SettingItem
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"} title="Trakt"
customIcon={<TraktIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />} description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
renderControl={ChevronRight} customIcon={<TraktIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
onPress={() => navigation.navigate('TraktSettings')} renderControl={ChevronRight}
isLast={true} onPress={() => navigation.navigate('TraktSettings')}
isTablet={isTablet} isLast={true}
/> isTablet={isTablet}
/>
)}
</SettingsCard> </SettingsCard>
); );
case 'content': case 'content':
return ( return (
<SettingsCard title="CONTENT & DISCOVERY" isTablet={isTablet}> <SettingsCard title="CONTENT & DISCOVERY" isTablet={isTablet}>
<SettingItem {isItemVisible('addons') && (
title="Addons" <SettingItem
description={`${addonCount} installed`} title="Addons"
icon="layers" description={`${addonCount} installed`}
renderControl={ChevronRight} icon="layers"
onPress={() => navigation.navigate('Addons')} renderControl={ChevronRight}
isTablet={isTablet} onPress={() => navigation.navigate('Addons')}
/> isTablet={isTablet}
<SettingItem />
title="Debrid Integration" )}
description="Connect Torbox for premium streams" {isItemVisible('debrid') && (
icon="link" <SettingItem
renderControl={ChevronRight} title="Debrid Integration"
onPress={() => navigation.navigate('DebridIntegration')} description="Connect Torbox for premium streams"
isTablet={isTablet} icon="link"
/> renderControl={ChevronRight}
<SettingItem onPress={() => navigation.navigate('DebridIntegration')}
title="Plugins" isTablet={isTablet}
description="Manage plugins and repositories" />
customIcon={<PluginIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />} )}
renderControl={ChevronRight} {isItemVisible('plugins') && (
onPress={() => navigation.navigate('ScraperSettings')} <SettingItem
isTablet={isTablet} title="Plugins"
/> description="Manage plugins and repositories"
<SettingItem customIcon={<PluginIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
title="Catalogs" renderControl={ChevronRight}
description={`${catalogCount} active`} onPress={() => navigation.navigate('ScraperSettings')}
icon="list" isTablet={isTablet}
renderControl={ChevronRight} />
onPress={() => navigation.navigate('CatalogSettings')} )}
isTablet={isTablet} {isItemVisible('catalogs') && (
/> <SettingItem
<SettingItem title="Catalogs"
title="Home Screen" description={`${catalogCount} active`}
description="Layout and content" icon="list"
icon="home" renderControl={ChevronRight}
renderControl={ChevronRight} onPress={() => navigation.navigate('CatalogSettings')}
onPress={() => navigation.navigate('HomeScreenSettings')} isTablet={isTablet}
isTablet={isTablet} />
/> )}
<SettingItem {isItemVisible('home_screen') && (
title="Show Discover Section" <SettingItem
description="Display discover content in Search" title="Home Screen"
icon="compass" description="Layout and content"
renderControl={() => ( icon="home"
<CustomSwitch renderControl={ChevronRight}
value={settings?.showDiscover ?? true} onPress={() => navigation.navigate('HomeScreenSettings')}
onValueChange={(value) => updateSetting('showDiscover', value)} isTablet={isTablet}
/> />
)} )}
isTablet={isTablet} {isItemVisible('show_discover') && (
/> <SettingItem
<SettingItem title="Show Discover Section"
title="Continue Watching" description="Display discover content in Search"
description="Cache and playback behavior" icon="compass"
icon="play-circle" renderControl={() => (
renderControl={ChevronRight} <CustomSwitch
onPress={() => navigation.navigate('ContinueWatchingSettings')} value={settings?.showDiscover ?? true}
isLast={true} onValueChange={(value) => updateSetting('showDiscover', value)}
isTablet={isTablet} />
/> )}
isTablet={isTablet}
/>
)}
{isItemVisible('continue_watching') && (
<SettingItem
title="Continue Watching"
description="Cache and playback behavior"
icon="play-circle"
renderControl={ChevronRight}
onPress={() => navigation.navigate('ContinueWatchingSettings')}
isLast={true}
isTablet={isTablet}
/>
)}
</SettingsCard> </SettingsCard>
); );
case 'appearance': case 'appearance':
return ( return (
<SettingsCard title="APPEARANCE" isTablet={isTablet}> <SettingsCard title="APPEARANCE" isTablet={isTablet}>
<SettingItem {isItemVisible('theme') && (
title="Theme" <SettingItem
description={currentTheme.name} title="Theme"
icon="sliders" description={currentTheme.name}
renderControl={ChevronRight} icon="sliders"
onPress={() => navigation.navigate('ThemeSettings')} renderControl={ChevronRight}
isTablet={isTablet} onPress={() => navigation.navigate('ThemeSettings')}
/> isTablet={isTablet}
<SettingItem />
title="Episode Layout" )}
description={settings?.episodeLayoutStyle === 'horizontal' ? 'Horizontal' : 'Vertical'} {isItemVisible('episode_layout') && (
icon="grid" <SettingItem
renderControl={() => ( title="Episode Layout"
<CustomSwitch description={settings?.episodeLayoutStyle === 'horizontal' ? 'Horizontal' : 'Vertical'}
value={settings?.episodeLayoutStyle === 'horizontal'} icon="grid"
onValueChange={(value) => updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')} renderControl={() => (
/> <CustomSwitch
)} value={settings?.episodeLayoutStyle === 'horizontal'}
isLast={isTablet} onValueChange={(value) => updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')}
isTablet={isTablet} />
/> )}
{!isTablet && ( isLast={isTablet}
isTablet={isTablet}
/>
)}
{!isTablet && isItemVisible('streams_backdrop') && (
<SettingItem <SettingItem
title="Streams Backdrop" title="Streams Backdrop"
description="Show blurred backdrop on mobile streams" description="Show blurred backdrop on mobile streams"
@ -663,92 +697,106 @@ const SettingsScreen: React.FC = () => {
case 'integrations': case 'integrations':
return ( return (
<SettingsCard title="INTEGRATIONS" isTablet={isTablet}> <SettingsCard title="INTEGRATIONS" isTablet={isTablet}>
<SettingItem {isItemVisible('mdblist') && (
title="MDBList" <SettingItem
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"} title="MDBList"
customIcon={<MDBListIcon size={isTablet ? 24 : 20} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />} description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
renderControl={ChevronRight} customIcon={<MDBListIcon size={isTablet ? 24 : 20} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />}
onPress={() => navigation.navigate('MDBListSettings')} renderControl={ChevronRight}
isTablet={isTablet} onPress={() => navigation.navigate('MDBListSettings')}
/> isTablet={isTablet}
<SettingItem />
title="TMDB" )}
description="Metadata & logo source provider" {isItemVisible('tmdb') && (
customIcon={<TMDBIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />} <SettingItem
renderControl={ChevronRight} title="TMDB"
onPress={() => navigation.navigate('TMDBSettings')} description="Metadata & logo source provider"
isLast={true} customIcon={<TMDBIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
isTablet={isTablet} renderControl={ChevronRight}
/> onPress={() => navigation.navigate('TMDBSettings')}
isLast={true}
isTablet={isTablet}
/>
)}
</SettingsCard> </SettingsCard>
); );
case 'ai': case 'ai':
return ( return (
<SettingsCard title="AI ASSISTANT" isTablet={isTablet}> <SettingsCard title="AI ASSISTANT" isTablet={isTablet}>
<SettingItem {isItemVisible('openrouter') && (
title="OpenRouter API" <SettingItem
description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"} title="OpenRouter API"
icon="cpu" description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"}
renderControl={ChevronRight} icon="cpu"
onPress={() => navigation.navigate('AISettings')} renderControl={ChevronRight}
isLast={true} onPress={() => navigation.navigate('AISettings')}
isTablet={isTablet} isLast={true}
/> isTablet={isTablet}
/>
)}
</SettingsCard> </SettingsCard>
); );
case 'playback': case 'playback':
return ( return (
<SettingsCard title="PLAYBACK" isTablet={isTablet}> <SettingsCard title="PLAYBACK" isTablet={isTablet}>
<SettingItem {isItemVisible('video_player') && (
title="Video Player" <SettingItem
description={Platform.OS === 'ios' title="Video Player"
? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in') description={Platform.OS === 'ios'
: (settings?.useExternalPlayer ? 'External' : 'Built-in') ? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in')
} : (settings?.useExternalPlayer ? 'External' : 'Built-in')
icon="play-circle" }
renderControl={ChevronRight} icon="play-circle"
onPress={() => navigation.navigate('PlayerSettings')} renderControl={ChevronRight}
isTablet={isTablet} onPress={() => navigation.navigate('PlayerSettings')}
/> isTablet={isTablet}
<SettingItem />
title="Show Trailers" )}
description="Display trailers in hero section" {isItemVisible('show_trailers') && (
icon="film" <SettingItem
renderControl={() => ( title="Show Trailers"
<Switch description="Display trailers in hero section"
value={settings?.showTrailers ?? true} icon="film"
onValueChange={(value) => updateSetting('showTrailers', value)} renderControl={() => (
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} <Switch
thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'} value={settings?.showTrailers ?? true}
/> onValueChange={(value) => updateSetting('showTrailers', value)}
)} trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
isTablet={isTablet} thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'}
/> />
<SettingItem )}
title="Enable Downloads (Beta)" isTablet={isTablet}
description="Show Downloads tab and enable saving streams" />
icon="download" )}
renderControl={() => ( {isItemVisible('enable_downloads') && (
<Switch <SettingItem
value={settings?.enableDownloads ?? false} title="Enable Downloads (Beta)"
onValueChange={(value) => updateSetting('enableDownloads', value)} description="Show Downloads tab and enable saving streams"
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} icon="download"
thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} renderControl={() => (
/> <Switch
)} value={settings?.enableDownloads ?? false}
isTablet={isTablet} onValueChange={(value) => updateSetting('enableDownloads', value)}
/> trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
<SettingItem thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'}
title="Notifications" />
description="Episode reminders" )}
icon="bell" isTablet={isTablet}
renderControl={ChevronRight} />
onPress={() => navigation.navigate('NotificationSettings')} )}
isLast={true} {isItemVisible('notifications') && (
isTablet={isTablet} <SettingItem
/> title="Notifications"
description="Episode reminders"
icon="bell"
renderControl={ChevronRight}
onPress={() => navigation.navigate('NotificationSettings')}
isLast={true}
isTablet={isTablet}
/>
)}
</SettingsCard> </SettingsCard>
); );
@ -1079,75 +1127,103 @@ const SettingsScreen: React.FC = () => {
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
> >
{/* Account */} {/* Account */}
<SettingsCard title="ACCOUNT"> {(settingsConfig?.categories?.['account']?.visible !== false) && isItemVisible('trakt') && (
<SettingItem <SettingsCard title="ACCOUNT">
title="Trakt" {isItemVisible('trakt') && (
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"} <SettingItem
customIcon={<TraktIcon size={20} color={currentTheme.colors.primary} />} title="Trakt"
renderControl={ChevronRight} description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
onPress={() => navigation.navigate('TraktSettings')} customIcon={<TraktIcon size={20} color={currentTheme.colors.primary} />}
isLast renderControl={ChevronRight}
/> onPress={() => navigation.navigate('TraktSettings')}
</SettingsCard> isLast
/>
)}
</SettingsCard>
)}
{/* General Settings */} {/* General Settings */}
<SettingsCard title="GENERAL"> {(
<SettingItem (settingsConfig?.categories?.['content']?.visible !== false) ||
title="Content & Discovery" (settingsConfig?.categories?.['appearance']?.visible !== false) ||
description="Addons, catalogs, and sources" (settingsConfig?.categories?.['integrations']?.visible !== false) ||
icon="compass" (settingsConfig?.categories?.['playback']?.visible !== false)
renderControl={ChevronRight} ) && (
onPress={() => navigation.navigate('ContentDiscoverySettings')} <SettingsCard title="GENERAL">
/> {(settingsConfig?.categories?.['content']?.visible !== false) && (
<SettingItem <SettingItem
title="Appearance" title="Content & Discovery"
description={currentTheme.name} description="Addons, catalogs, and sources"
icon="sliders" icon="compass"
renderControl={ChevronRight} renderControl={ChevronRight}
onPress={() => navigation.navigate('AppearanceSettings')} onPress={() => navigation.navigate('ContentDiscoverySettings')}
/> />
<SettingItem )}
title="Integrations" {(settingsConfig?.categories?.['appearance']?.visible !== false) && (
description="MDBList, TMDB, AI" <SettingItem
icon="layers" title="Appearance"
renderControl={ChevronRight} description={currentTheme.name}
onPress={() => navigation.navigate('IntegrationsSettings')} icon="sliders"
/> renderControl={ChevronRight}
<SettingItem onPress={() => navigation.navigate('AppearanceSettings')}
title="Playback" />
description="Player, trailers, downloads" )}
icon="play-circle" {(settingsConfig?.categories?.['integrations']?.visible !== false) && (
renderControl={ChevronRight} <SettingItem
onPress={() => navigation.navigate('PlaybackSettings')} title="Integrations"
isLast description="MDBList, TMDB, AI"
/> icon="layers"
</SettingsCard> renderControl={ChevronRight}
onPress={() => navigation.navigate('IntegrationsSettings')}
/>
)}
{(settingsConfig?.categories?.['playback']?.visible !== false) && (
<SettingItem
title="Playback"
description="Player, trailers, downloads"
icon="play-circle"
renderControl={ChevronRight}
onPress={() => navigation.navigate('PlaybackSettings')}
isLast
/>
)}
</SettingsCard>
)}
{/* Data */} {/* Data */}
<SettingsCard title="DATA"> {(
<SettingItem (settingsConfig?.categories?.['backup']?.visible !== false) ||
title="Backup & Restore" (settingsConfig?.categories?.['updates']?.visible !== false)
description="Create and restore app backups" ) && (
icon="archive" <SettingsCard title="DATA">
renderControl={ChevronRight} {(settingsConfig?.categories?.['backup']?.visible !== false) && (
onPress={() => navigation.navigate('Backup')} <SettingItem
/> title="Backup & Restore"
<SettingItem description="Create and restore app backups"
title="App Updates" icon="archive"
description="Check for updates" renderControl={ChevronRight}
icon="refresh-ccw" onPress={() => navigation.navigate('Backup')}
badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined} />
renderControl={ChevronRight} )}
onPress={async () => { {(settingsConfig?.categories?.['updates']?.visible !== false) && (
if (Platform.OS === 'android') { <SettingItem
try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { } title="App Updates"
setHasUpdateBadge(false); description="Check for updates"
} icon="refresh-ccw"
navigation.navigate('Update'); badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined}
}} renderControl={ChevronRight}
isLast onPress={async () => {
/> if (Platform.OS === 'android') {
</SettingsCard> try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { }
setHasUpdateBadge(false);
}
navigation.navigate('Update');
}}
isLast
/>
)}
</SettingsCard>
)}
{/* Cache - only if MDBList is set */} {/* Cache - only if MDBList is set */}
{mdblistKeySet && ( {mdblistKeySet && (
@ -1188,7 +1264,7 @@ const SettingsScreen: React.FC = () => {
)} )}
{/* Downloads Counter */} {/* Downloads Counter */}
{displayDownloads !== null && ( {settingsConfig?.items?.['downloads_counter']?.visible !== false && displayDownloads !== null && (
<View style={styles.downloadsContainer}> <View style={styles.downloadsContainer}>
<Text style={[styles.downloadsNumber, { color: currentTheme.colors.primary }]}> <Text style={[styles.downloadsNumber, { color: currentTheme.colors.primary }]}>
{displayDownloads.toLocaleString()} {displayDownloads.toLocaleString()}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React, { useState, useCallback } from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform, Dimensions } from 'react-native'; import { View, StyleSheet, ScrollView, StatusBar, Platform, Dimensions } from 'react-native';
import { useNavigation } 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';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
@ -8,6 +8,7 @@ import { useSettings } from '../../hooks/useSettings';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
import ScreenHeader from '../../components/common/ScreenHeader'; import ScreenHeader from '../../components/common/ScreenHeader';
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const isTablet = width >= 768; const isTablet = width >= 768;
@ -17,6 +18,21 @@ const AppearanceSettingsScreen: React.FC = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const config = useRealtimeConfig();
const isItemVisible = (itemId: string) => {
if (!config?.items) return true;
const item = config.items[itemId];
if (item && item.visible === false) return false;
return true;
};
const hasVisibleItems = (itemIds: string[]) => {
return itemIds.some(id => {
if (id === 'streams_backdrop' && isTablet) return false;
return isItemVisible(id);
});
};
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -28,45 +44,53 @@ const AppearanceSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
<SettingsCard title="THEME"> {hasVisibleItems(['theme']) && (
<SettingItem <SettingsCard title="THEME">
title="Theme" {isItemVisible('theme') && (
description={currentTheme.name} <SettingItem
icon="sliders" title="Theme"
renderControl={() => <ChevronRight />} description={currentTheme.name}
onPress={() => navigation.navigate('ThemeSettings')} icon="sliders"
isLast renderControl={() => <ChevronRight />}
/> onPress={() => navigation.navigate('ThemeSettings')}
</SettingsCard> isLast
<SettingsCard title="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} </SettingsCard>
/> )}
{!isTablet && (
<SettingItem {hasVisibleItems(['episode_layout', 'streams_backdrop']) && (
title="Streams Backdrop" <SettingsCard title="LAYOUT">
description="Show blurred backdrop on mobile streams" {isItemVisible('episode_layout') && (
icon="image" <SettingItem
renderControl={() => ( title="Episode Layout"
<CustomSwitch description={settings?.episodeLayoutStyle === 'horizontal' ? 'Horizontal' : 'Vertical'}
value={settings?.enableStreamsBackdrop ?? true} icon="grid"
onValueChange={(value) => updateSetting('enableStreamsBackdrop', value)} renderControl={() => (
/> <CustomSwitch
)} value={settings?.episodeLayoutStyle === 'horizontal'}
isLast onValueChange={(value) => updateSetting('episodeLayoutStyle', value ? 'horizontal' : 'vertical')}
/> />
)} )}
</SettingsCard> 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,6 +1,6 @@
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 } from 'react-native';
import { useNavigation } 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';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
@ -11,6 +11,7 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
import ScreenHeader from '../../components/common/ScreenHeader'; import ScreenHeader from '../../components/common/ScreenHeader';
import PluginIcon from '../../components/icons/PluginIcon'; 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';
const ContentDiscoverySettingsScreen: React.FC = () => { const ContentDiscoverySettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -20,6 +21,7 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
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 {
@ -48,16 +50,22 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
} }
}, []); }, []);
useEffect(() => { useFocusEffect(
loadData(); useCallback(() => {
}, [loadData]);
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
loadData(); loadData();
}); }, [loadData])
return unsubscribe; );
}, [navigation, loadData]);
const isItemVisible = (itemId: string) => {
if (!config?.items) return true;
const item = config.items[itemId];
if (item && item.visible === false) return false;
return true;
};
const hasVisibleItems = (itemIds: string[]) => {
return itemIds.some(id => isItemVisible(id));
};
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -69,70 +77,90 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
<SettingsCard title="SOURCES"> {hasVisibleItems(['addons', 'debrid', 'plugins']) && (
<SettingItem <SettingsCard title="SOURCES">
title="Addons" {isItemVisible('addons') && (
description={`${addonCount} installed`} <SettingItem
icon="layers" title="Addons"
renderControl={() => <ChevronRight />} description={`${addonCount} installed`}
onPress={() => navigation.navigate('Addons')} icon="layers"
/> renderControl={() => <ChevronRight />}
<SettingItem onPress={() => navigation.navigate('Addons')}
title="Debrid Integration"
description="Connect Torbox for premium streams"
icon="link"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('DebridIntegration')}
/>
<SettingItem
title="Plugins"
description="Manage plugins and repositories"
customIcon={<PluginIcon size={18} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ScraperSettings')}
isLast
/>
</SettingsCard>
<SettingsCard title="CATALOGS">
<SettingItem
title="Catalogs"
description={`${catalogCount} active`}
icon="list"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('CatalogSettings')}
/>
<SettingItem
title="Home Screen"
description="Layout and content"
icon="home"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('HomeScreenSettings')}
/>
<SettingItem
title="Continue Watching"
description="Cache and playback behavior"
icon="play-circle"
renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('ContinueWatchingSettings')}
isLast
/>
</SettingsCard>
<SettingsCard title="DISCOVERY">
<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 {isItemVisible('debrid') && (
/> <SettingItem
</SettingsCard> 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,6 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { View, StyleSheet, ScrollView, StatusBar } from 'react-native'; import { View, StyleSheet, ScrollView, StatusBar } from 'react-native';
import { useNavigation } 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';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
@ -10,6 +10,7 @@ import ScreenHeader from '../../components/common/ScreenHeader';
import MDBListIcon from '../../components/icons/MDBListIcon'; import MDBListIcon from '../../components/icons/MDBListIcon';
import TMDBIcon from '../../components/icons/TMDBIcon'; import TMDBIcon from '../../components/icons/TMDBIcon';
import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents'; import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents';
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
const IntegrationsSettingsScreen: React.FC = () => { const IntegrationsSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -18,6 +19,7 @@ const IntegrationsSettingsScreen: React.FC = () => {
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 {
@ -31,16 +33,22 @@ const IntegrationsSettingsScreen: React.FC = () => {
} }
}, []); }, []);
useEffect(() => { useFocusEffect(
loadData(); useCallback(() => {
}, [loadData]);
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
loadData(); loadData();
}); }, [loadData])
return unsubscribe; );
}, [navigation, loadData]);
const isItemVisible = (itemId: string) => {
if (!config?.items) return true;
const item = config.items[itemId];
if (item && item.visible === false) return false;
return true;
};
const hasVisibleItems = (itemIds: string[]) => {
return itemIds.some(id => isItemVisible(id));
};
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -52,34 +60,44 @@ const IntegrationsSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
<SettingsCard title="METADATA"> {hasVisibleItems(['mdblist', 'tmdb']) && (
<SettingItem <SettingsCard title="METADATA">
title="MDBList" {isItemVisible('mdblist') && (
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"} <SettingItem
customIcon={<MDBListIcon size={18} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />} title="MDBList"
renderControl={() => <ChevronRight />} description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
onPress={() => navigation.navigate('MDBListSettings')} customIcon={<MDBListIcon size={18} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />}
/> renderControl={() => <ChevronRight />}
<SettingItem onPress={() => navigation.navigate('MDBListSettings')}
title="TMDB" />
description="Metadata & logo source provider" )}
customIcon={<TMDBIcon size={18} color={currentTheme.colors.primary} />} {isItemVisible('tmdb') && (
renderControl={() => <ChevronRight />} <SettingItem
onPress={() => navigation.navigate('TMDBSettings')} title="TMDB"
isLast description="Metadata & logo source provider"
/> customIcon={<TMDBIcon size={18} color={currentTheme.colors.primary} />}
</SettingsCard> renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('TMDBSettings')}
isLast
/>
)}
</SettingsCard>
)}
<SettingsCard title="AI ASSISTANT"> {hasVisibleItems(['openrouter']) && (
<SettingItem <SettingsCard title="AI ASSISTANT">
title="OpenRouter API" {isItemVisible('openrouter') && (
description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"} <SettingItem
icon="cpu" title="OpenRouter API"
renderControl={() => <ChevronRight />} description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"}
onPress={() => navigation.navigate('AISettings')} icon="cpu"
isLast renderControl={() => <ChevronRight />}
/> onPress={() => navigation.navigate('AISettings')}
</SettingsCard> isLast
/>
)}
</SettingsCard>
)}
</ScrollView> </ScrollView>
</View> </View>
); );

View file

@ -1,6 +1,6 @@
import React from 'react'; import React, { useState, useCallback } from 'react';
import { View, StyleSheet, ScrollView, StatusBar, Platform } from 'react-native'; import { View, StyleSheet, ScrollView, StatusBar, Platform } from 'react-native';
import { useNavigation } 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';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
@ -8,12 +8,25 @@ import { useSettings } from '../../hooks/useSettings';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
import ScreenHeader from '../../components/common/ScreenHeader'; import ScreenHeader from '../../components/common/ScreenHeader';
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents'; import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
const PlaybackSettingsScreen: React.FC = () => { const PlaybackSettingsScreen: React.FC = () => {
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 insets = useSafeAreaInsets();
const config = useRealtimeConfig();
const isItemVisible = (itemId: string) => {
if (!config?.items) return true;
const item = config.items[itemId];
if (item && item.visible === false) return false;
return true;
};
const hasVisibleItems = (itemIds: string[]) => {
return itemIds.some(id => isItemVisible(id));
};
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
@ -25,56 +38,70 @@ const PlaybackSettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
> >
<SettingsCard title="VIDEO PLAYER"> {hasVisibleItems(['video_player']) && (
<SettingItem <SettingsCard title="VIDEO PLAYER">
title="Video Player" {isItemVisible('video_player') && (
description={Platform.OS === 'ios' <SettingItem
? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in') title="Video Player"
: (settings?.useExternalPlayer ? 'External' : 'Built-in') description={Platform.OS === 'ios'
} ? (settings?.preferredPlayer === 'internal' ? 'Built-in' : settings?.preferredPlayer?.toUpperCase() || 'Built-in')
icon="play-circle" : (settings?.useExternalPlayer ? 'External' : 'Built-in')
renderControl={() => <ChevronRight />} }
onPress={() => navigation.navigate('PlayerSettings')} icon="play-circle"
isLast renderControl={() => <ChevronRight />}
/> onPress={() => navigation.navigate('PlayerSettings')}
</SettingsCard> isLast
<SettingsCard title="MEDIA">
<SettingItem
title="Show Trailers"
description="Display trailers in hero section"
icon="film"
renderControl={() => (
<CustomSwitch
value={settings?.showTrailers ?? true}
onValueChange={(value) => updateSetting('showTrailers', value)}
/> />
)} )}
/> </SettingsCard>
<SettingItem )}
title="Enable Downloads (Beta)"
description="Show Downloads tab and enable saving streams" {hasVisibleItems(['show_trailers', 'enable_downloads']) && (
icon="download" <SettingsCard title="MEDIA">
renderControl={() => ( {isItemVisible('show_trailers') && (
<CustomSwitch <SettingItem
value={settings?.enableDownloads ?? false} title="Show Trailers"
onValueChange={(value) => updateSetting('enableDownloads', value)} description="Display trailers in hero section"
icon="film"
renderControl={() => (
<CustomSwitch
value={settings?.showTrailers ?? true}
onValueChange={(value) => updateSetting('showTrailers', value)}
/>
)}
/> />
)} )}
isLast {isItemVisible('enable_downloads') && (
/> <SettingItem
</SettingsCard> 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>
)}
<SettingsCard title="NOTIFICATIONS"> {hasVisibleItems(['notifications']) && (
<SettingItem <SettingsCard title="NOTIFICATIONS">
title="Notifications" {isItemVisible('notifications') && (
description="Episode reminders" <SettingItem
icon="bell" title="Notifications"
renderControl={() => <ChevronRight />} description="Episode reminders"
onPress={() => navigation.navigate('NotificationSettings')} icon="bell"
isLast renderControl={() => <ChevronRight />}
/> onPress={() => navigation.navigate('NotificationSettings')}
</SettingsCard> isLast
/>
)}
</SettingsCard>
)}
</ScrollView> </ScrollView>
</View> </View>
); );

View file

@ -0,0 +1,59 @@
import { Platform } from 'react-native';
// Reuse the same base URL as campaign service
const CAMPAIGN_API_URL = process.env.EXPO_PUBLIC_CAMPAIGN_API_URL || 'http://localhost:3000';
export interface SettingsConfig {
categories?: {
[key: string]: {
visible?: boolean;
order?: number;
title?: string;
}
};
items?: {
[key: string]: {
visible?: boolean;
[key: string]: any;
}
};
}
class ConfigService {
private configCache: Record<string, any> = {};
async getConfig<T>(key: string): Promise<T | null> {
// Return memory cache if available (fetch once per session)
if (this.configCache[key]) {
return this.configCache[key] as T;
}
try {
console.log(`[ConfigService] Fetching config for key: ${key}`);
const response = await fetch(`${CAMPAIGN_API_URL}/api/config?key=${key}`);
if (!response.ok) {
return null;
}
const data = await response.json();
// If data is empty object, return null
if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
return null;
}
this.configCache[key] = data;
return data as T;
} catch (error) {
console.warn('[ConfigService] Error fetching config:', error);
return null;
}
}
async getSettingsConfig(): Promise<SettingsConfig | null> {
return this.getConfig<SettingsConfig>('settings_screen');
}
}
export const configService = new ConfigService();