mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
more sdui control over settinsgcreen.
This commit is contained in:
parent
44abb9f635
commit
4173786b12
7 changed files with 692 additions and 430 deletions
30
src/hooks/useRealtimeConfig.ts
Normal file
30
src/hooks/useRealtimeConfig.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { useRealtimeConfig } from '../hooks/useRealtimeConfig';
|
||||
|
||||
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -316,11 +320,13 @@ const SettingsScreen: React.FC = () => {
|
|||
const [catalogCount, setCatalogCount] = useState<number>(0);
|
||||
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
|
||||
const [openRouterKeySet, setOpenRouterKeySet] = useState<boolean>(false);
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState<boolean>(false);
|
||||
const [totalDownloads, setTotalDownloads] = useState<number | null>(null);
|
||||
const [totalDownloads, setTotalDownloads] = useState<number>(0);
|
||||
const [displayDownloads, setDisplayDownloads] = useState<number | null>(null);
|
||||
const [isCountingUp, setIsCountingUp] = useState<boolean>(false);
|
||||
|
||||
// Use Realtime Config Hook
|
||||
const settingsConfig = useRealtimeConfig();
|
||||
|
||||
// Scroll to top ref and handler
|
||||
const mobileScrollViewRef = useRef<ScrollView>(null);
|
||||
const tabletScrollViewRef = useRef<ScrollView>(null);
|
||||
|
|
@ -354,7 +360,6 @@ const SettingsScreen: React.FC = () => {
|
|||
// Load addon count and get their catalogs
|
||||
const addons = await stremioService.getInstalledAddonsAsync();
|
||||
setAddonCount(addons.length);
|
||||
setInitialLoadComplete(true);
|
||||
|
||||
// Count total available catalogs
|
||||
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
|
||||
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 === 'cache' && !mdblistKeySet) return false;
|
||||
return true;
|
||||
|
|
@ -539,110 +553,130 @@ const SettingsScreen: React.FC = () => {
|
|||
case 'account':
|
||||
return (
|
||||
<SettingsCard title="ACCOUNT" isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title="Trakt"
|
||||
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
|
||||
customIcon={<TraktIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
{isItemVisible('trakt') && (
|
||||
<SettingItem
|
||||
title="Trakt"
|
||||
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
|
||||
customIcon={<TraktIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
);
|
||||
|
||||
case 'content':
|
||||
return (
|
||||
<SettingsCard title="CONTENT & DISCOVERY" isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title="Addons"
|
||||
description={`${addonCount} installed`}
|
||||
icon="layers"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('Addons')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Debrid Integration"
|
||||
description="Connect Torbox for premium streams"
|
||||
icon="link"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('DebridIntegration')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Plugins"
|
||||
description="Manage plugins and repositories"
|
||||
customIcon={<PluginIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ScraperSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Catalogs"
|
||||
description={`${catalogCount} active`}
|
||||
icon="list"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('CatalogSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Home Screen"
|
||||
description="Layout and content"
|
||||
icon="home"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('HomeScreenSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Show Discover Section"
|
||||
description="Display discover content in Search"
|
||||
icon="compass"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings?.showDiscover ?? true}
|
||||
onValueChange={(value) => updateSetting('showDiscover', value)}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Continue Watching"
|
||||
description="Cache and playback behavior"
|
||||
icon="play-circle"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ContinueWatchingSettings')}
|
||||
isLast={true}
|
||||
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 ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ScraperSettings')}
|
||||
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('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)}
|
||||
/>
|
||||
)}
|
||||
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>
|
||||
);
|
||||
|
||||
case 'appearance':
|
||||
return (
|
||||
<SettingsCard title="APPEARANCE" isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title="Theme"
|
||||
description={currentTheme.name}
|
||||
icon="sliders"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ThemeSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<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}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
{!isTablet && (
|
||||
{isItemVisible('theme') && (
|
||||
<SettingItem
|
||||
title="Theme"
|
||||
description={currentTheme.name}
|
||||
icon="sliders"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ThemeSettings')}
|
||||
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}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
{!isTablet && isItemVisible('streams_backdrop') && (
|
||||
<SettingItem
|
||||
title="Streams Backdrop"
|
||||
description="Show blurred backdrop on mobile streams"
|
||||
|
|
@ -663,92 +697,106 @@ const SettingsScreen: React.FC = () => {
|
|||
case 'integrations':
|
||||
return (
|
||||
<SettingsCard title="INTEGRATIONS" isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title="MDBList"
|
||||
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
|
||||
customIcon={<MDBListIcon size={isTablet ? 24 : 20} colorPrimary={currentTheme.colors.primary} colorSecondary={currentTheme.colors.white} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('MDBListSettings')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="TMDB"
|
||||
description="Metadata & logo source provider"
|
||||
customIcon={<TMDBIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TMDBSettings')}
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
{isItemVisible('mdblist') && (
|
||||
<SettingItem
|
||||
title="MDBList"
|
||||
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
|
||||
customIcon={<MDBListIcon size={isTablet ? 24 : 20} 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 ? 24 : 20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TMDBSettings')}
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
);
|
||||
|
||||
case 'ai':
|
||||
return (
|
||||
<SettingsCard title="AI ASSISTANT" isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title="OpenRouter API"
|
||||
description={openRouterKeySet ? "Connected" : "Add your API key to enable AI chat"}
|
||||
icon="cpu"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('AISettings')}
|
||||
isLast={true}
|
||||
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={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
);
|
||||
|
||||
case 'playback':
|
||||
return (
|
||||
<SettingsCard title="PLAYBACK" isTablet={isTablet}>
|
||||
<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')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Show Trailers"
|
||||
description="Display trailers in hero section"
|
||||
icon="film"
|
||||
renderControl={() => (
|
||||
<Switch
|
||||
value={settings?.showTrailers ?? true}
|
||||
onValueChange={(value) => updateSetting('showTrailers', value)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
|
||||
thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Enable Downloads (Beta)"
|
||||
description="Show Downloads tab and enable saving streams"
|
||||
icon="download"
|
||||
renderControl={() => (
|
||||
<Switch
|
||||
value={settings?.enableDownloads ?? false}
|
||||
onValueChange={(value) => updateSetting('enableDownloads', value)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
|
||||
thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Notifications"
|
||||
description="Episode reminders"
|
||||
icon="bell"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('NotificationSettings')}
|
||||
isLast={true}
|
||||
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')}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
{isItemVisible('show_trailers') && (
|
||||
<SettingItem
|
||||
title="Show Trailers"
|
||||
description="Display trailers in hero section"
|
||||
icon="film"
|
||||
renderControl={() => (
|
||||
<Switch
|
||||
value={settings?.showTrailers ?? true}
|
||||
onValueChange={(value) => updateSetting('showTrailers', value)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
|
||||
thumbColor={settings?.showTrailers ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
{isItemVisible('enable_downloads') && (
|
||||
<SettingItem
|
||||
title="Enable Downloads (Beta)"
|
||||
description="Show Downloads tab and enable saving streams"
|
||||
icon="download"
|
||||
renderControl={() => (
|
||||
<Switch
|
||||
value={settings?.enableDownloads ?? false}
|
||||
onValueChange={(value) => updateSetting('enableDownloads', value)}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }}
|
||||
thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
{isItemVisible('notifications') && (
|
||||
<SettingItem
|
||||
title="Notifications"
|
||||
description="Episode reminders"
|
||||
icon="bell"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('NotificationSettings')}
|
||||
isLast={true}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
);
|
||||
|
||||
|
|
@ -1079,75 +1127,103 @@ const SettingsScreen: React.FC = () => {
|
|||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Account */}
|
||||
<SettingsCard title="ACCOUNT">
|
||||
<SettingItem
|
||||
title="Trakt"
|
||||
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
|
||||
customIcon={<TraktIcon size={20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
{(settingsConfig?.categories?.['account']?.visible !== false) && isItemVisible('trakt') && (
|
||||
<SettingsCard title="ACCOUNT">
|
||||
{isItemVisible('trakt') && (
|
||||
<SettingItem
|
||||
title="Trakt"
|
||||
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
|
||||
customIcon={<TraktIcon size={20} color={currentTheme.colors.primary} />}
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* General Settings */}
|
||||
<SettingsCard title="GENERAL">
|
||||
<SettingItem
|
||||
title="Content & Discovery"
|
||||
description="Addons, catalogs, and sources"
|
||||
icon="compass"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ContentDiscoverySettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Appearance"
|
||||
description={currentTheme.name}
|
||||
icon="sliders"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('AppearanceSettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Integrations"
|
||||
description="MDBList, TMDB, AI"
|
||||
icon="layers"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('IntegrationsSettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Playback"
|
||||
description="Player, trailers, downloads"
|
||||
icon="play-circle"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('PlaybackSettings')}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
{(
|
||||
(settingsConfig?.categories?.['content']?.visible !== false) ||
|
||||
(settingsConfig?.categories?.['appearance']?.visible !== false) ||
|
||||
(settingsConfig?.categories?.['integrations']?.visible !== false) ||
|
||||
(settingsConfig?.categories?.['playback']?.visible !== false)
|
||||
) && (
|
||||
<SettingsCard title="GENERAL">
|
||||
{(settingsConfig?.categories?.['content']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title="Content & Discovery"
|
||||
description="Addons, catalogs, and sources"
|
||||
icon="compass"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ContentDiscoverySettings')}
|
||||
/>
|
||||
)}
|
||||
{(settingsConfig?.categories?.['appearance']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title="Appearance"
|
||||
description={currentTheme.name}
|
||||
icon="sliders"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('AppearanceSettings')}
|
||||
/>
|
||||
)}
|
||||
{(settingsConfig?.categories?.['integrations']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title="Integrations"
|
||||
description="MDBList, TMDB, AI"
|
||||
icon="layers"
|
||||
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 */}
|
||||
<SettingsCard title="DATA">
|
||||
<SettingItem
|
||||
title="Backup & Restore"
|
||||
description="Create and restore app backups"
|
||||
icon="archive"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('Backup')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="App Updates"
|
||||
description="Check for updates"
|
||||
icon="refresh-ccw"
|
||||
badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined}
|
||||
renderControl={ChevronRight}
|
||||
onPress={async () => {
|
||||
if (Platform.OS === 'android') {
|
||||
try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { }
|
||||
setHasUpdateBadge(false);
|
||||
}
|
||||
navigation.navigate('Update');
|
||||
}}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
{(
|
||||
(settingsConfig?.categories?.['backup']?.visible !== false) ||
|
||||
(settingsConfig?.categories?.['updates']?.visible !== false)
|
||||
) && (
|
||||
<SettingsCard title="DATA">
|
||||
{(settingsConfig?.categories?.['backup']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title="Backup & Restore"
|
||||
description="Create and restore app backups"
|
||||
icon="archive"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('Backup')}
|
||||
/>
|
||||
)}
|
||||
{(settingsConfig?.categories?.['updates']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title="App Updates"
|
||||
description="Check for updates"
|
||||
icon="refresh-ccw"
|
||||
badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined}
|
||||
renderControl={ChevronRight}
|
||||
onPress={async () => {
|
||||
if (Platform.OS === 'android') {
|
||||
try { await mmkvStorage.removeItem('@update_badge_pending'); } catch { }
|
||||
setHasUpdateBadge(false);
|
||||
}
|
||||
navigation.navigate('Update');
|
||||
}}
|
||||
isLast
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* Cache - only if MDBList is set */}
|
||||
{mdblistKeySet && (
|
||||
|
|
@ -1188,7 +1264,7 @@ const SettingsScreen: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* Downloads Counter */}
|
||||
{displayDownloads !== null && (
|
||||
{settingsConfig?.items?.['downloads_counter']?.visible !== false && displayDownloads !== null && (
|
||||
<View style={styles.downloadsContainer}>
|
||||
<Text style={[styles.downloadsNumber, { color: currentTheme.colors.primary }]}>
|
||||
{displayDownloads.toLocaleString()}
|
||||
|
|
|
|||
|
|
@ -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 { useNavigation } from '@react-navigation/native';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -8,6 +8,7 @@ import { useSettings } from '../../hooks/useSettings';
|
|||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import ScreenHeader from '../../components/common/ScreenHeader';
|
||||
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
||||
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -17,6 +18,21 @@ const AppearanceSettingsScreen: React.FC = () => {
|
|||
const { currentTheme } = useTheme();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
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 (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
|
|
@ -28,45 +44,53 @@ const AppearanceSettingsScreen: React.FC = () => {
|
|||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
|
||||
>
|
||||
<SettingsCard title="THEME">
|
||||
<SettingItem
|
||||
title="Theme"
|
||||
description={currentTheme.name}
|
||||
icon="sliders"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('ThemeSettings')}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<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')}
|
||||
{hasVisibleItems(['theme']) && (
|
||||
<SettingsCard title="THEME">
|
||||
{isItemVisible('theme') && (
|
||||
<SettingItem
|
||||
title="Theme"
|
||||
description={currentTheme.name}
|
||||
icon="sliders"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('ThemeSettings')}
|
||||
isLast
|
||||
/>
|
||||
)}
|
||||
isLast={isTablet}
|
||||
/>
|
||||
{!isTablet && (
|
||||
<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>
|
||||
</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>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -11,6 +11,7 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
|
|||
import ScreenHeader from '../../components/common/ScreenHeader';
|
||||
import PluginIcon from '../../components/icons/PluginIcon';
|
||||
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
||||
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
|
||||
|
||||
const ContentDiscoverySettingsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -20,6 +21,7 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
|
|||
|
||||
const [addonCount, setAddonCount] = useState<number>(0);
|
||||
const [catalogCount, setCatalogCount] = useState<number>(0);
|
||||
const config = useRealtimeConfig();
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -48,16 +50,22 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadData();
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [navigation, loadData]);
|
||||
}, [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 (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
|
|
@ -69,70 +77,90 @@ const ContentDiscoverySettingsScreen: React.FC = () => {
|
|||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
|
||||
>
|
||||
<SettingsCard title="SOURCES">
|
||||
<SettingItem
|
||||
title="Addons"
|
||||
description={`${addonCount} installed`}
|
||||
icon="layers"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('Addons')}
|
||||
/>
|
||||
<SettingItem
|
||||
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)}
|
||||
{hasVisibleItems(['addons', 'debrid', 'plugins']) && (
|
||||
<SettingsCard title="SOURCES">
|
||||
{isItemVisible('addons') && (
|
||||
<SettingItem
|
||||
title="Addons"
|
||||
description={`${addonCount} installed`}
|
||||
icon="layers"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('Addons')}
|
||||
/>
|
||||
)}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
{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>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -10,6 +10,7 @@ import ScreenHeader from '../../components/common/ScreenHeader';
|
|||
import MDBListIcon from '../../components/icons/MDBListIcon';
|
||||
import TMDBIcon from '../../components/icons/TMDBIcon';
|
||||
import { SettingsCard, SettingItem, ChevronRight } from './SettingsComponents';
|
||||
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
|
||||
|
||||
const IntegrationsSettingsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -18,6 +19,7 @@ const IntegrationsSettingsScreen: React.FC = () => {
|
|||
|
||||
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
|
||||
const [openRouterKeySet, setOpenRouterKeySet] = useState<boolean>(false);
|
||||
const config = useRealtimeConfig();
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -31,16 +33,22 @@ const IntegrationsSettingsScreen: React.FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadData();
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [navigation, loadData]);
|
||||
}, [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 (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
|
|
@ -52,34 +60,44 @@ const IntegrationsSettingsScreen: React.FC = () => {
|
|||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
|
||||
>
|
||||
<SettingsCard title="METADATA">
|
||||
<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')}
|
||||
/>
|
||||
<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(['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>
|
||||
)}
|
||||
|
||||
<SettingsCard title="AI ASSISTANT">
|
||||
<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>
|
||||
{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>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -8,12 +8,25 @@ import { useSettings } from '../../hooks/useSettings';
|
|||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import ScreenHeader from '../../components/common/ScreenHeader';
|
||||
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
||||
import { useRealtimeConfig } from '../../hooks/useRealtimeConfig';
|
||||
|
||||
const PlaybackSettingsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
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 (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
|
|
@ -25,56 +38,70 @@ const PlaybackSettingsScreen: React.FC = () => {
|
|||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 24 }]}
|
||||
>
|
||||
<SettingsCard title="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>
|
||||
|
||||
<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)}
|
||||
{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
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<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)}
|
||||
</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)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<SettingsCard title="NOTIFICATIONS">
|
||||
<SettingItem
|
||||
title="Notifications"
|
||||
description="Episode reminders"
|
||||
icon="bell"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('NotificationSettings')}
|
||||
isLast
|
||||
/>
|
||||
</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>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
59
src/services/configService.ts
Normal file
59
src/services/configService.ts
Normal 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();
|
||||
Loading…
Reference in a new issue