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 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()}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
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