NuvioStreaming/src/screens/SettingsScreen.tsx
tapframe e686caebb8 Add logo source preference setting and enhance logo fetching logic in MetadataScreen
Introduce a new setting for logo source preference in the SettingsScreen, allowing users to choose between 'metahub' and 'tmdb' as the primary source for title logos. Update the MetadataScreen to utilize this preference, implementing logic to refresh logos based on the selected source. Enhance error handling and logging during logo fetching to improve user experience and debugging capabilities.
2025-05-03 17:56:47 +05:30

678 lines
No EOL
22 KiB
TypeScript

import React, { useCallback, useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Switch,
ScrollView,
useColorScheme,
SafeAreaView,
StatusBar,
Alert,
Platform,
Dimensions,
Image
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { Picker } from '@react-native-picker/picker';
import { colors } from '../styles/colors';
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
import { stremioService } from '../services/stremioService';
import { useCatalogContext } from '../contexts/CatalogContext';
import { useTraktContext } from '../contexts/TraktContext';
import { catalogService, DataSource } from '../services/catalogService';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width } = Dimensions.get('window');
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
// Card component with modern style
interface SettingsCardProps {
children: React.ReactNode;
isDarkMode: boolean;
title?: string;
}
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => (
<View style={[styles.cardContainer]}>
{title && (
<Text style={[
styles.cardTitle,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
]}>
{title.toUpperCase()}
</Text>
)}
<View style={[
styles.card,
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
]}>
{children}
</View>
</View>
);
interface SettingItemProps {
title: string;
description?: string;
icon: string;
renderControl: () => React.ReactNode;
isLast?: boolean;
onPress?: () => void;
isDarkMode: boolean;
badge?: string | number;
}
const SettingItem: React.FC<SettingItemProps> = ({
title,
description,
icon,
renderControl,
isLast = false,
onPress,
isDarkMode,
badge
}) => {
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={onPress}
style={[
styles.settingItem,
!isLast && styles.settingItemBorder,
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)' }
]}
>
<View style={[
styles.settingIconContainer,
{ backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }
]}>
<MaterialIcons name={icon} size={20} color={colors.primary} />
</View>
<View style={styles.settingContent}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
{title}
</Text>
{description && (
<Text style={[styles.settingDescription, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
{description}
</Text>
)}
</View>
{badge && (
<View style={[styles.badge, { backgroundColor: colors.primary }]}>
<Text style={styles.badgeText}>{badge}</Text>
</View>
)}
</View>
<View style={styles.settingControl}>
{renderControl()}
</View>
</TouchableOpacity>
);
};
const SettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
const systemColorScheme = useColorScheme();
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { lastUpdate } = useCatalogContext();
const { isAuthenticated, userProfile } = useTraktContext();
const insets = useSafeAreaInsets();
// States for dynamic content
const [addonCount, setAddonCount] = useState<number>(0);
const [catalogCount, setCatalogCount] = useState<number>(0);
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
const [discoverDataSource, setDiscoverDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
// Force consistent status bar settings
useEffect(() => {
const applyStatusBarConfig = () => {
StatusBar.setBarStyle('light-content');
if (Platform.OS === 'android') {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
}
};
applyStatusBarConfig();
// Re-apply on focus
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
return unsubscribe;
}, [navigation]);
const loadData = useCallback(async () => {
try {
// Load addon count and get their catalogs
const addons = await stremioService.getInstalledAddonsAsync();
setAddonCount(addons.length);
// Count total available catalogs
let totalCatalogs = 0;
addons.forEach(addon => {
if (addon.catalogs && addon.catalogs.length > 0) {
totalCatalogs += addon.catalogs.length;
}
});
// Load saved catalog settings
const catalogSettingsJson = await AsyncStorage.getItem('catalog_settings');
if (catalogSettingsJson) {
const catalogSettings = JSON.parse(catalogSettingsJson);
// Filter out _lastUpdate key and count only explicitly disabled catalogs
const disabledCount = Object.entries(catalogSettings)
.filter(([key, value]) => key !== '_lastUpdate' && value === false)
.length;
// Since catalogs are enabled by default, subtract disabled ones from total
setCatalogCount(totalCatalogs - disabledCount);
} else {
// If no settings saved, all catalogs are enabled by default
setCatalogCount(totalCatalogs);
}
// Check MDBList API key status
const mdblistKey = await AsyncStorage.getItem('mdblist_api_key');
setMdblistKeySet(!!mdblistKey);
// Get discover data source preference
const dataSource = await catalogService.getDataSourcePreference();
setDiscoverDataSource(dataSource);
} catch (error) {
console.error('Error loading settings data:', error);
}
}, []);
// Load data initially and when catalogs are updated
useEffect(() => {
loadData();
}, [loadData, lastUpdate]);
// Add focus listener to reload data when screen comes into focus
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
loadData();
});
return unsubscribe;
}, [navigation, loadData]);
const handleResetSettings = useCallback(() => {
Alert.alert(
'Reset Settings',
'Are you sure you want to reset all settings to default values?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Reset',
style: 'destructive',
onPress: () => {
(Object.keys(DEFAULT_SETTINGS) as Array<keyof typeof DEFAULT_SETTINGS>).forEach(key => {
updateSetting(key, DEFAULT_SETTINGS[key]);
});
}
}
]
);
}, [updateSetting]);
const CustomSwitch = ({ value, onValueChange }: { value: boolean, onValueChange: (value: boolean) => void }) => (
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', true: colors.primary }}
thumbColor={Platform.OS === 'android' ? (value ? colors.white : colors.white) : ''}
ios_backgroundColor={isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
/>
);
const ChevronRight = () => (
<MaterialIcons
name="chevron-right"
size={22}
color={isDarkMode ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.3)'}
/>
);
// Handle data source change
const handleDiscoverDataSourceChange = useCallback(async (value: string) => {
const dataSource = value as DataSource;
setDiscoverDataSource(dataSource);
await catalogService.setDataSourcePreference(dataSource);
}, []);
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return (
<View style={[
styles.container,
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
]}>
{/* Fixed position header background to prevent shifts */}
<View style={[
styles.headerBackground,
{ height: headerHeight, backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
]} />
<View style={{ flex: 1 }}>
{/* Header Section with proper top spacing */}
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Settings
</Text>
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
<Text style={[styles.resetButtonText, {color: colors.primary}]}>Reset</Text>
</TouchableOpacity>
</View>
{/* Content Container */}
<View style={styles.contentContainer}>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<SettingsCard isDarkMode={isDarkMode} title="User & Account">
<SettingItem
title="Trakt"
description={isAuthenticated ? `Connected as ${userProfile?.username || 'User'}` : "Not Connected"}
icon="person"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TraktSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Features">
<SettingItem
title="Calendar"
description="Manage your show calendar settings"
icon="calendar-today"
renderControl={ChevronRight}
onPress={() => navigation.navigate('Calendar')}
isDarkMode={isDarkMode}
/>
<SettingItem
title="Notifications"
description="Configure episode notifications and reminders"
icon="notifications"
renderControl={ChevronRight}
onPress={() => navigation.navigate('NotificationSettings')}
isDarkMode={isDarkMode}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Content">
<SettingItem
title="Addons"
description="Manage your installed addons"
icon="extension"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('Addons')}
badge={addonCount}
/>
<SettingItem
title="Catalogs"
description="Configure content sources"
icon="view-list"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('CatalogSettings')}
badge={catalogCount}
/>
<SettingItem
title="Home Screen"
description="Customize layout and content"
icon="home"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('HomeScreenSettings')}
/>
<SettingItem
title="Ratings Source"
description={mdblistKeySet ? "MDBList API Configured" : "Configure MDBList API"}
icon="info-outline"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('MDBListSettings')}
/>
<SettingItem
title="Logo Source Preference"
description="Choose primary source for title logos"
icon="image"
isDarkMode={isDarkMode}
renderControl={() => (
<View style={styles.selectorContainer}>
<TouchableOpacity
style={[
styles.selectorButton,
settings.logoSourcePreference === 'metahub' && styles.selectorButtonActive
]}
onPress={() => {
console.log('Setting logo source preference to Metahub');
updateSetting('logoSourcePreference', 'metahub');
console.log('New logo source preference:', 'metahub');
// Clear any cached logo data in storage
try {
// This is just to help clear any cached state - the exact implementation may vary
AsyncStorage.removeItem('_last_logos_');
} catch (e) {
console.error('Error clearing logo cache:', e);
}
// Show alert that settings have been updated
Alert.alert(
'Settings Updated',
'Logo source preference set to Metahub. Changes will apply when you navigate to content.',
[{ text: 'OK' }]
);
}}
>
<Text style={[
styles.selectorText,
settings.logoSourcePreference === 'metahub' && styles.selectorTextActive
]}>Metahub</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
settings.logoSourcePreference === 'tmdb' && styles.selectorButtonActive
]}
onPress={() => {
console.log('Setting logo source preference to TMDB');
updateSetting('logoSourcePreference', 'tmdb');
console.log('New logo source preference:', 'tmdb');
// Clear any cached logo data in storage
try {
// This is just to help clear any cached state - the exact implementation may vary
AsyncStorage.removeItem('_last_logos_');
} catch (e) {
console.error('Error clearing logo cache:', e);
}
// Show alert that settings have been updated
Alert.alert(
'Settings Updated',
'Logo source preference set to TMDB. Changes will apply when you navigate to content.',
[{ text: 'OK' }]
);
}}
>
<Text style={[
styles.selectorText,
settings.logoSourcePreference === 'tmdb' && styles.selectorTextActive
]}>TMDB</Text>
</TouchableOpacity>
</View>
)}
/>
<SettingItem
title="TMDB"
description="API & Metadata Settings"
icon="movie-filter"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TMDBSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Playback">
<SettingItem
title="Video Player"
description={Platform.OS === 'ios'
? (settings.preferredPlayer === 'internal'
? 'Built-in Player'
: settings.preferredPlayer
? settings.preferredPlayer.toUpperCase()
: 'Built-in Player')
: (settings.useExternalPlayer ? 'External Player' : 'Built-in Player')
}
icon="play-arrow"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('PlayerSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Discover">
<SettingItem
title="Content Source"
description="Choose where to get content for the Discover screen"
icon="explore"
isDarkMode={isDarkMode}
renderControl={() => (
<View style={styles.selectorContainer}>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
]}>Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
]}>TMDB</Text>
</TouchableOpacity>
</View>
)}
/>
</SettingsCard>
<View style={styles.versionContainer}>
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
Version 1.0.0
</Text>
</View>
</ScrollView>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1,
},
contentContainer: {
flex: 1,
zIndex: 1,
width: '100%',
},
header: {
paddingHorizontal: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
paddingBottom: 8,
backgroundColor: 'transparent',
zIndex: 2,
},
headerTitle: {
fontSize: 32,
fontWeight: '800',
letterSpacing: 0.3,
},
resetButton: {
paddingVertical: 8,
paddingHorizontal: 12,
},
resetButtonText: {
fontSize: 16,
fontWeight: '600',
},
scrollView: {
flex: 1,
width: '100%',
},
scrollContent: {
flexGrow: 1,
width: '100%',
paddingBottom: 32,
},
cardContainer: {
width: '100%',
marginBottom: 20,
},
cardTitle: {
fontSize: 13,
fontWeight: '600',
letterSpacing: 0.8,
marginLeft: 16,
marginBottom: 8,
},
card: {
marginHorizontal: 16,
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
width: undefined, // Let it fill the container width
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderBottomWidth: 0.5,
minHeight: 58,
width: '100%',
},
settingItemBorder: {
// Border styling handled directly in the component with borderBottomWidth
},
settingIconContainer: {
marginRight: 16,
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
settingContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
settingTextContainer: {
flex: 1,
},
settingTitleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
settingTitle: {
fontSize: 16,
fontWeight: '500',
marginBottom: 3,
},
settingDescription: {
fontSize: 14,
opacity: 0.8,
},
settingControl: {
justifyContent: 'center',
alignItems: 'center',
paddingLeft: 12,
},
badge: {
height: 22,
minWidth: 22,
borderRadius: 11,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
marginRight: 8,
},
badgeText: {
color: 'white',
fontSize: 12,
fontWeight: '600',
},
versionContainer: {
alignItems: 'center',
justifyContent: 'center',
marginTop: 10,
marginBottom: 20,
},
versionText: {
fontSize: 14,
},
pickerContainer: {
flex: 1,
},
picker: {
flex: 1,
},
selectorContainer: {
flexDirection: 'row',
borderRadius: 8,
overflow: 'hidden',
height: 36,
width: 160,
marginRight: 8,
},
selectorButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 12,
backgroundColor: 'rgba(255,255,255,0.08)',
},
selectorButtonActive: {
backgroundColor: colors.primary,
},
selectorText: {
fontSize: 14,
fontWeight: '500',
color: colors.mediumEmphasis,
},
selectorTextActive: {
color: colors.white,
fontWeight: '600',
},
});
export default SettingsScreen;