Implement theme context integration across components for improved UI consistency

Refactor various components to utilize the new ThemeContext for dynamic theming. This includes updating styles in the App, NuvioHeader, CatalogSection, and other components to reflect the current theme colors. Additionally, introduce a ThemedApp component to centralize theme management and enhance the overall user experience by ensuring consistent styling throughout the application. Update package dependencies to include react-native-wheel-color-picker for enhanced color selection capabilities.
This commit is contained in:
tapframe 2025-05-03 21:49:20 +05:30
parent 843d31707c
commit ed358c85fe
23 changed files with 1577 additions and 722 deletions

62
App.tsx
View file

@ -23,33 +23,61 @@ import 'react-native-reanimated';
import { CatalogProvider } from './src/contexts/CatalogContext';
import { GenreProvider } from './src/contexts/GenreContext';
import { TraktProvider } from './src/contexts/TraktContext';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
// This fixes many navigation layout issues by using native screen containers
enableScreens(true);
function App(): React.JSX.Element {
// Always use dark mode
const isDarkMode = true;
// Inner app component that uses the theme context
const ThemedApp = () => {
const { currentTheme } = useTheme();
// Create custom themes based on current theme
const customDarkTheme = {
...CustomDarkTheme,
colors: {
...CustomDarkTheme.colors,
primary: currentTheme.colors.primary,
}
};
const customNavigationTheme = {
...CustomNavigationDarkTheme,
colors: {
...CustomNavigationDarkTheme.colors,
primary: currentTheme.colors.primary,
card: currentTheme.colors.darkBackground,
background: currentTheme.colors.darkBackground,
}
};
return (
<PaperProvider theme={customDarkTheme}>
<NavigationContainer
theme={customNavigationTheme}
// Disable automatic linking which can cause layout issues
linking={undefined}
>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
style="light"
/>
<AppNavigator />
</View>
</NavigationContainer>
</PaperProvider>
);
}
function App(): React.JSX.Element {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<GenreProvider>
<CatalogProvider>
<TraktProvider>
<PaperProvider theme={CustomDarkTheme}>
<NavigationContainer
theme={CustomNavigationDarkTheme}
// Disable automatic linking which can cause layout issues
linking={undefined}
>
<View style={[styles.container, { backgroundColor: '#000000' }]}>
<StatusBar
style="light"
/>
<AppNavigator />
</View>
</NavigationContainer>
</PaperProvider>
<ThemeProvider>
<ThemedApp />
</ThemeProvider>
</TraktProvider>
</CatalogProvider>
</GenreProvider>

16
package-lock.json generated
View file

@ -61,6 +61,7 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1",
"subsrt": "^1.1.1"
},
"devDependencies": {
@ -10840,6 +10841,12 @@
"react-native-reanimated": ">=2.8.0"
}
},
"node_modules/react-native-elevation": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-native-elevation/-/react-native-elevation-1.0.0.tgz",
"integrity": "sha512-BWIKcEYtzjRV6GpkX0Km5/w2E7fgIcywiQOT7JZTc5NSbv/YI9kpFinB9lRFsOoRVGmiqq/O3VfP/oH2clIiBA==",
"license": "MIT"
},
"node_modules/react-native-gesture-handler": {
"version": "2.20.2",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
@ -11197,6 +11204,15 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-native-wheel-color-picker": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-wheel-color-picker/-/react-native-wheel-color-picker-1.3.1.tgz",
"integrity": "sha512-ojuajzwEkgIHa4Iw94K9FlwA1iifslMo+HDrOFQMBTMCXm1HaFhtQpDZ5upV9y8vujviDko3hDkVqB7/eV0dzg==",
"license": "MIT",
"dependencies": {
"react-native-elevation": "^1.0.0"
}
},
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz",

View file

@ -62,6 +62,7 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1",
"subsrt": "^1.1.1"
},
"devDependencies": {

View file

@ -1,19 +1,20 @@
import React from 'react';
import { View, TouchableOpacity, Platform, StyleSheet, Image } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors } from '../styles/colors';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from '../navigation/AppNavigator';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
import Constants, { ExecutionEnvironment } from 'expo-constants';
import { useTheme } from '../contexts/ThemeContext';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
export const NuvioHeader = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute();
const { currentTheme } = useTheme();
// Only render the header if the current route is 'Home'
if (route.name !== 'Home') {
@ -59,7 +60,7 @@ export const NuvioHeader = () => {
<MaterialCommunityIcons
name="magnify"
size={24}
color={colors.white}
color={currentTheme.colors.white}
/>
</View>
</TouchableOpacity>

View file

@ -3,7 +3,7 @@ import { View, Text, TouchableOpacity, StyleSheet, FlatList, Dimensions } from '
import { MaterialIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { colors } from '../../styles';
import { useTheme } from '../../contexts/ThemeContext';
import { GenreCatalog, Category } from '../../constants/discover';
import { StreamingContent } from '../../services/catalogService';
import { RootStackParamList } from '../../navigation/AppNavigator';
@ -16,6 +16,7 @@ interface CatalogSectionProps {
const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const { width } = Dimensions.get('window');
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
@ -56,16 +57,18 @@ const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{catalog.genre}</Text>
<View style={styles.titleBar} />
<Text style={[styles.title, { color: currentTheme.colors.white }]}>
{catalog.genre}
</Text>
<View style={[styles.titleBar, { backgroundColor: currentTheme.colors.primary }]} />
</View>
<TouchableOpacity
onPress={handleSeeMorePress}
style={styles.seeAllButton}
activeOpacity={0.6}
>
<Text style={styles.seeAllText}>See All</Text>
<MaterialIcons name="arrow-forward-ios" color={colors.primary} size={14} />
<Text style={[styles.seeAllText, { color: currentTheme.colors.primary }]}>See All</Text>
<MaterialIcons name="arrow-forward-ios" color={currentTheme.colors.primary} size={14} />
</TouchableOpacity>
</View>
@ -106,14 +109,12 @@ const styles = StyleSheet.create({
titleBar: {
width: 32,
height: 3,
backgroundColor: colors.primary,
marginTop: 6,
borderRadius: 2,
},
title: {
fontSize: 20,
fontWeight: '700',
color: colors.white,
},
seeAllButton: {
flexDirection: 'row',
@ -123,7 +124,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
},
seeAllText: {
color: colors.primary,
fontWeight: '600',
fontSize: 14,
},

View file

@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../../styles';
import { useTheme } from '../../contexts/ThemeContext';
import { Category } from '../../constants/discover';
interface CategorySelectorProps {
@ -15,6 +15,7 @@ const CategorySelector = ({
selectedCategory,
onSelectCategory
}: CategorySelectorProps) => {
const { currentTheme } = useTheme();
const renderCategoryButton = useCallback((category: Category) => {
const isSelected = selectedCategory.id === category.id;
@ -24,7 +25,7 @@ const CategorySelector = ({
key={category.id}
style={[
styles.categoryButton,
isSelected && styles.selectedCategoryButton
isSelected && { backgroundColor: currentTheme.colors.primary }
]}
onPress={() => onSelectCategory(category)}
activeOpacity={0.7}
@ -32,19 +33,19 @@ const CategorySelector = ({
<MaterialIcons
name={category.icon}
size={24}
color={isSelected ? colors.white : colors.mediumGray}
color={isSelected ? currentTheme.colors.white : currentTheme.colors.mediumGray}
/>
<Text
style={[
styles.categoryText,
isSelected && styles.selectedCategoryText
isSelected && { color: currentTheme.colors.white, fontWeight: '700' }
]}
>
{category.name}
</Text>
</TouchableOpacity>
);
}, [selectedCategory, onSelectCategory]);
}, [selectedCategory, onSelectCategory, currentTheme]);
return (
<View style={styles.container}>
@ -78,24 +79,17 @@ const styles = StyleSheet.create({
flex: 1,
maxWidth: 160,
justifyContent: 'center',
shadowColor: colors.black,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4,
},
selectedCategoryButton: {
backgroundColor: colors.primary,
},
categoryText: {
color: colors.mediumGray,
color: '#9e9e9e', // Default medium gray
fontWeight: '600',
fontSize: 16,
},
selectedCategoryText: {
color: colors.white,
fontWeight: '700',
},
});
export default React.memo(CategorySelector);

View file

@ -2,7 +2,7 @@ import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { colors } from '../../styles';
import { useTheme } from '../../contexts/ThemeContext';
import { StreamingContent } from '../../services/catalogService';
interface ContentItemProps {
@ -13,6 +13,7 @@ interface ContentItemProps {
const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
const { width: screenWidth } = Dimensions.get('window');
const { currentTheme } = useTheme();
const itemWidth = width || (screenWidth - 48) / 2.2; // Default to 2 items per row with spacing
return (
@ -21,7 +22,7 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
onPress={onPress}
activeOpacity={0.6}
>
<View style={styles.posterContainer}>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
<Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
@ -33,7 +34,7 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
colors={['transparent', 'rgba(0,0,0,0.85)']}
style={styles.gradient}
>
<Text style={styles.title} numberOfLines={2}>
<Text style={[styles.title, { color: currentTheme.colors.white }]} numberOfLines={2}>
{item.name}
</Text>
{item.year && (
@ -54,7 +55,6 @@ const styles = StyleSheet.create({
overflow: 'hidden',
backgroundColor: 'rgba(255,255,255,0.03)',
elevation: 5,
shadowColor: colors.black,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
@ -75,7 +75,6 @@ const styles = StyleSheet.create({
title: {
fontSize: 15,
fontWeight: '700',
color: colors.white,
marginBottom: 4,
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: 0, height: 1 },

View file

@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
import { colors } from '../../styles';
import { useTheme } from '../../contexts/ThemeContext';
interface GenreSelectorProps {
genres: string[];
@ -13,6 +13,7 @@ const GenreSelector = ({
selectedGenre,
onSelectGenre
}: GenreSelectorProps) => {
const { currentTheme } = useTheme();
const renderGenreButton = useCallback((genre: string) => {
const isSelected = selectedGenre === genre;
@ -22,7 +23,7 @@ const GenreSelector = ({
key={genre}
style={[
styles.genreButton,
isSelected && styles.selectedGenreButton
isSelected && { backgroundColor: currentTheme.colors.primary }
]}
onPress={() => onSelectGenre(genre)}
activeOpacity={0.7}
@ -30,14 +31,14 @@ const GenreSelector = ({
<Text
style={[
styles.genreText,
isSelected && styles.selectedGenreText
isSelected && { color: currentTheme.colors.white, fontWeight: '600' }
]}
>
{genre}
</Text>
</TouchableOpacity>
);
}, [selectedGenre, onSelectGenre]);
}, [selectedGenre, onSelectGenre, currentTheme]);
return (
<View style={styles.container}>
@ -70,25 +71,18 @@ const styles = StyleSheet.create({
marginRight: 12,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.05)',
shadowColor: colors.black,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
overflow: 'hidden',
},
selectedGenreButton: {
backgroundColor: colors.primary,
},
genreText: {
color: colors.mediumGray,
color: '#9e9e9e', // Default medium gray
fontWeight: '500',
fontSize: 14,
},
selectedGenreText: {
color: colors.white,
fontWeight: '600',
},
});
export default React.memo(GenreSelector);

View file

@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform } from 'react-native';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../../styles/colors';
import { useTheme } from '../../contexts/ThemeContext';
import { catalogService, StreamingContent } from '../../services/catalogService';
import DropUpMenu from './DropUpMenu';
@ -20,6 +20,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const { currentTheme } = useTheme();
const handleLongPress = useCallback(() => {
setMenuVisible(true);
@ -95,22 +96,22 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
}}
/>
{(!imageLoaded || imageError) && (
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
{!imageError ? (
<ActivityIndicator color={colors.primary} size="small" />
<ActivityIndicator color={currentTheme.colors.primary} size="small" />
) : (
<MaterialIcons name="broken-image" size={24} color={colors.lightGray} />
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.lightGray} />
)}
</View>
)}
{isWatched && (
<View style={styles.watchedIndicator}>
<MaterialIcons name="check-circle" size={22} color={colors.success} />
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
</View>
)}
{localItem.inLibrary && (
<View style={styles.libraryBadge}>
<MaterialIcons name="bookmark" size={16} color={colors.white} />
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
</View>
)}
</View>
@ -160,7 +161,6 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 16,
@ -169,7 +169,6 @@ const styles = StyleSheet.create({
position: 'absolute',
top: 8,
right: 8,
backgroundColor: colors.transparentDark,
borderRadius: 12,
padding: 2,
},
@ -177,7 +176,6 @@ const styles = StyleSheet.create({
position: 'absolute',
top: 8,
left: 8,
backgroundColor: colors.transparentDark,
borderRadius: 8,
padding: 4,
},

View file

@ -17,7 +17,6 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../../styles/colors';
import Animated, {
FadeIn,
useAnimatedStyle,
@ -32,6 +31,8 @@ import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from
import { useSettings } from '../../hooks/useSettings';
import { TMDBService } from '../../services/tmdbService';
import { logger } from '../../utils/logger';
import { useTheme } from '../../contexts/ThemeContext';
import type { Theme } from '../../contexts/ThemeContext';
interface FeaturedContentProps {
featuredContent: StreamingContent | null;
@ -47,6 +48,7 @@ const { width, height } = Dimensions.get('window');
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { settings } = useSettings();
const { currentTheme } = useTheme();
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
const prevContentIdRef = useRef<string | null>(null);
@ -350,7 +352,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
'transparent',
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.7)',
colors.darkBackground,
currentTheme.colors.darkBackground,
]}
locations={[0, 0.3, 0.7, 1]}
style={styles.featuredGradient as ViewStyle}
@ -373,14 +375,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
/>
</Animated.View>
) : (
<Text style={styles.featuredTitleText as TextStyle}>{featuredContent.name}</Text>
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={styles.genreText as TextStyle}>{genre}</Text>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={styles.genreDot as TextStyle}></Text>
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
@ -395,15 +401,15 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={colors.white}
color={currentTheme.colors.white}
/>
<Text style={styles.myListButtonText as TextStyle}>
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.playButton as ViewStyle}
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => {
if (featuredContent) {
navigation.navigate('Streams', {
@ -413,8 +419,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
}
}}
>
<MaterialIcons name="play-arrow" size={24} color={colors.black} />
<Text style={styles.playButtonText as TextStyle}>Play</Text>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -428,8 +436,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
}
}}
>
<MaterialIcons name="info-outline" size={24} color={colors.white} />
<Text style={styles.infoButtonText as TextStyle}>Info</Text>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
@ -446,7 +456,6 @@ const styles = StyleSheet.create({
marginTop: 0,
marginBottom: 8,
position: 'relative',
backgroundColor: colors.elevation1,
},
imageContainer: {
width: '100%',
@ -468,7 +477,6 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
bottom: 0,
backgroundColor: colors.elevation1,
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
@ -491,7 +499,6 @@ const styles = StyleSheet.create({
alignSelf: 'center',
},
featuredTitleText: {
color: colors.highEmphasis,
fontSize: 28,
fontWeight: '900',
marginBottom: 8,
@ -510,13 +517,11 @@ const styles = StyleSheet.create({
gap: 4,
},
genreText: {
color: colors.white,
fontSize: 14,
fontWeight: '500',
opacity: 0.9,
},
genreDot: {
color: colors.white,
fontSize: 14,
fontWeight: '500',
opacity: 0.6,
@ -538,7 +543,6 @@ const styles = StyleSheet.create({
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 30,
backgroundColor: colors.white,
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
@ -568,18 +572,15 @@ const styles = StyleSheet.create({
flex: undefined,
},
playButtonText: {
color: colors.black,
fontWeight: '600',
marginLeft: 8,
fontSize: 16,
},
myListButtonText: {
color: colors.white,
fontSize: 12,
fontWeight: '500',
},
infoButtonText: {
color: colors.white,
fontSize: 12,
fontWeight: '500',
},

View file

@ -1,23 +1,30 @@
import React from 'react';
import { View, Text, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';
import { colors } from '../../styles/colors';
import { useTheme } from '../../contexts/ThemeContext';
import type { Theme } from '../../contexts/ThemeContext';
const { height } = Dimensions.get('window');
export const SkeletonCatalog = () => (
<View style={styles.catalogContainer}>
<View style={styles.loadingPlaceholder}>
<ActivityIndicator size="small" color={colors.primary} />
export const SkeletonCatalog = () => {
const { currentTheme } = useTheme();
return (
<View style={styles.catalogContainer}>
<View style={[styles.loadingPlaceholder, { backgroundColor: currentTheme.colors.elevation1 }]}>
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View>
</View>
</View>
);
);
};
export const SkeletonFeatured = () => (
<View style={styles.featuredLoadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading featured content...</Text>
</View>
);
export const SkeletonFeatured = () => {
const { currentTheme } = useTheme();
return (
<View style={[styles.featuredLoadingContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading featured content...</Text>
</View>
);
};
const styles = StyleSheet.create({
catalogContainer: {
@ -29,7 +36,6 @@ const styles = StyleSheet.create({
height: 200,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.elevation1,
borderRadius: 12,
marginHorizontal: 16,
},
@ -37,28 +43,23 @@ const styles = StyleSheet.create({
height: height * 0.4,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.elevation1,
},
loadingText: {
color: colors.textMuted,
marginTop: 12,
fontSize: 14,
},
skeletonBox: {
backgroundColor: colors.elevation2,
borderRadius: 16,
overflow: 'hidden',
},
skeletonFeatured: {
width: '100%',
height: height * 0.6,
backgroundColor: colors.elevation2,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
marginBottom: 0,
},
skeletonPoster: {
backgroundColor: colors.elevation1,
marginHorizontal: 4,
borderRadius: 16,
},

View file

@ -0,0 +1,233 @@
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors as defaultColors } from '../styles/colors';
// Define the Theme interface
export interface Theme {
id: string;
name: string;
colors: typeof defaultColors;
isEditable: boolean;
}
// Default built-in themes
export const DEFAULT_THEMES: Theme[] = [
{
id: 'default',
name: 'Default Dark',
colors: defaultColors,
isEditable: false,
},
{
id: 'ocean',
name: 'Ocean Blue',
colors: {
...defaultColors,
primary: '#3498db',
secondary: '#2ecc71',
darkBackground: '#0a192f',
},
isEditable: false,
},
{
id: 'sunset',
name: 'Sunset',
colors: {
...defaultColors,
primary: '#ff7e5f',
secondary: '#feb47b',
darkBackground: '#1a0f0b',
},
isEditable: false,
},
{
id: 'moonlight',
name: 'Moonlight',
colors: {
...defaultColors,
primary: '#a786df',
secondary: '#5e72e4',
darkBackground: '#0f0f1a',
},
isEditable: false,
},
];
// Theme context props
interface ThemeContextProps {
currentTheme: Theme;
availableThemes: Theme[];
setCurrentTheme: (themeId: string) => void;
addCustomTheme: (theme: Omit<Theme, 'id' | 'isEditable'>) => void;
updateCustomTheme: (theme: Theme) => void;
deleteCustomTheme: (themeId: string) => void;
}
// Create the context
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
// Storage keys
const CURRENT_THEME_KEY = 'current_theme';
const CUSTOM_THEMES_KEY = 'custom_themes';
// Provider component
export function ThemeProvider({ children }: { children: ReactNode }) {
const [currentTheme, setCurrentThemeState] = useState<Theme>(DEFAULT_THEMES[0]);
const [availableThemes, setAvailableThemes] = useState<Theme[]>(DEFAULT_THEMES);
// Load themes from AsyncStorage on mount
useEffect(() => {
const loadThemes = async () => {
try {
// Load current theme ID
const savedThemeId = await AsyncStorage.getItem(CURRENT_THEME_KEY);
// Load custom themes
const customThemesJson = await AsyncStorage.getItem(CUSTOM_THEMES_KEY);
const customThemes = customThemesJson ? JSON.parse(customThemesJson) : [];
// Combine default and custom themes
const allThemes = [...DEFAULT_THEMES, ...customThemes];
setAvailableThemes(allThemes);
// Set current theme
if (savedThemeId) {
const theme = allThemes.find(t => t.id === savedThemeId);
if (theme) {
setCurrentThemeState(theme);
}
}
} catch (error) {
console.error('Failed to load themes:', error);
}
};
loadThemes();
}, []);
// Set current theme
const setCurrentTheme = async (themeId: string) => {
const theme = availableThemes.find(t => t.id === themeId);
if (theme) {
setCurrentThemeState(theme);
await AsyncStorage.setItem(CURRENT_THEME_KEY, themeId);
}
};
// Add custom theme
const addCustomTheme = async (themeData: Omit<Theme, 'id' | 'isEditable'>) => {
try {
// Generate unique ID
const id = `custom_${Date.now()}`;
// Create new theme object
const newTheme: Theme = {
id,
...themeData,
isEditable: true,
};
// Add to available themes
const customThemes = availableThemes.filter(t => t.isEditable);
const updatedCustomThemes = [...customThemes, newTheme];
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
// Save to storage
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
// Update state
setAvailableThemes(updatedAllThemes);
// Set as current theme
setCurrentThemeState(newTheme);
await AsyncStorage.setItem(CURRENT_THEME_KEY, id);
} catch (error) {
console.error('Failed to add custom theme:', error);
}
};
// Update custom theme
const updateCustomTheme = async (updatedTheme: Theme) => {
try {
if (!updatedTheme.isEditable) {
throw new Error('Cannot edit built-in themes');
}
// Find and update the theme
const customThemes = availableThemes.filter(t => t.isEditable);
const updatedCustomThemes = customThemes.map(t =>
t.id === updatedTheme.id ? updatedTheme : t
);
// Update available themes
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
// Save to storage
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
// Update state
setAvailableThemes(updatedAllThemes);
// Update current theme if needed
if (currentTheme.id === updatedTheme.id) {
setCurrentThemeState(updatedTheme);
}
} catch (error) {
console.error('Failed to update custom theme:', error);
}
};
// Delete custom theme
const deleteCustomTheme = async (themeId: string) => {
try {
// Find theme to delete
const themeToDelete = availableThemes.find(t => t.id === themeId);
if (!themeToDelete || !themeToDelete.isEditable) {
throw new Error('Cannot delete built-in themes or theme not found');
}
// Filter out the theme
const customThemes = availableThemes.filter(t => t.isEditable && t.id !== themeId);
const updatedAllThemes = [...DEFAULT_THEMES, ...customThemes];
// Save to storage
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(customThemes));
// Update state
setAvailableThemes(updatedAllThemes);
// Reset to default theme if current theme was deleted
if (currentTheme.id === themeId) {
setCurrentThemeState(DEFAULT_THEMES[0]);
await AsyncStorage.setItem(CURRENT_THEME_KEY, DEFAULT_THEMES[0].id);
}
} catch (error) {
console.error('Failed to delete custom theme:', error);
}
};
return (
<ThemeContext.Provider
value={{
currentTheme,
availableThemes,
setCurrentTheme,
addCustomTheme,
updateCustomTheme,
deleteCustomTheme,
}}
>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use the theme context
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View file

@ -13,6 +13,8 @@ import { colors } from '../styles/colors';
import { NuvioHeader } from '../components/NuvioHeader';
import { Stream } from '../types/streams';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { AnimationFade, AnimationSlideHorizontal } from '../utils/animations';
import { useTheme } from '../contexts/ThemeContext';
// Import screens with their proper types
import HomeScreen from '../screens/HomeScreen';
@ -36,6 +38,7 @@ import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
import LogoSourceSettings from '../screens/LogoSourceSettings';
import ThemeScreen from '../screens/ThemeScreen';
// Stack navigator types
export type RootStackParamList = {
@ -92,6 +95,7 @@ export type RootStackParamList = {
TraktSettings: undefined;
PlayerSettings: undefined;
LogoSourceSettings: undefined;
ThemeSettings: undefined;
};
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
@ -653,8 +657,7 @@ const MainTabs = () => {
// Stack Navigator
const AppNavigator = () => {
// Always use dark mode
const isDarkMode = true;
const { currentTheme } = useTheme();
return (
<SafeAreaProvider>
@ -671,7 +674,7 @@ const AppNavigator = () => {
animation: 'none',
// Ensure content is not popping in and out
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
}
}}
>
@ -723,7 +726,7 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
@ -738,7 +741,7 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
@ -776,7 +779,7 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
@ -791,7 +794,7 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
@ -806,7 +809,7 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
@ -821,7 +824,7 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
@ -836,7 +839,22 @@ const AppNavigator = () => {
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
<Stack.Screen
name="ThemeSettings"
component={ThemeScreen}
options={{
animation: 'fade',
animationDuration: 200,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>

View file

@ -12,11 +12,11 @@ import {
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../styles';
import { catalogService, StreamingContent } from '../services/catalogService';
import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
// Components
import CategorySelector from '../components/discover/CategorySelector';
@ -37,6 +37,7 @@ const DiscoverScreen = () => {
const [loading, setLoading] = useState(true);
const styles = useDiscoverStyles();
const insets = useSafeAreaInsets();
const { currentTheme } = useTheme();
// Force consistent status bar settings
useEffect(() => {
@ -162,7 +163,7 @@ const DiscoverScreen = () => {
<MaterialIcons
name="search"
size={24}
color={colors.white}
color={currentTheme.colors.white}
/>
</TouchableOpacity>
</View>
@ -187,7 +188,7 @@ const DiscoverScreen = () => {
{/* Content Section */}
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
</View>
) : catalogs.length > 0 ? (
<CatalogsList

View file

@ -26,7 +26,6 @@ import { Stream } from '../types/metadata';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { Image as ExpoImage } from 'expo-image';
import { colors } from '../styles/colors';
import Animated, {
FadeIn,
FadeOut,
@ -58,7 +57,9 @@ import { useSettings, settingsEmitter } from '../hooks/useSettings';
import FeaturedContent from '../components/home/FeaturedContent';
import CatalogSection from '../components/home/CatalogSection';
import { SkeletonFeatured } from '../components/home/SkeletonLoaders';
import homeStyles from '../styles/homeStyles';
import homeStyles, { sharedStyles } from '../styles/homeStyles';
import { useTheme } from '../contexts/ThemeContext';
import type { Theme } from '../contexts/ThemeContext';
// Define interfaces for our data
interface Category {
@ -86,6 +87,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
const translateY = useSharedValue(300);
const opacity = useSharedValue(0);
const isDarkMode = useColorScheme() === 'dark';
const { currentTheme } = useTheme();
const SNAP_THRESHOLD = 100;
useEffect(() => {
@ -126,12 +128,14 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
const overlayStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
backgroundColor: currentTheme.colors.transparentDark,
}));
const menuStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white,
}));
const menuOptions = [
@ -157,8 +161,6 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
}
];
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
return (
<Modal
visible={visible}
@ -170,20 +172,20 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.menuContainer, menuStyle, { backgroundColor }]}>
<View style={styles.dragHandle} />
<View style={styles.menuHeader}>
<Animated.View style={[styles.menuContainer, menuStyle]}>
<View style={[styles.dragHandle, { backgroundColor: currentTheme.colors.transparentLight }]} />
<View style={[styles.menuHeader, { borderBottomColor: currentTheme.colors.border }]}>
<ExpoImage
source={{ uri: item.poster }}
style={styles.menuPoster}
contentFit="cover"
/>
<View style={styles.menuTitleContainer}>
<Text style={[styles.menuTitle, { color: isDarkMode ? '#FFFFFF' : '#000000' }]}>
<Text style={[styles.menuTitle, { color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }]}>
{item.name}
</Text>
{item.year && (
<Text style={[styles.menuYear, { color: isDarkMode ? '#999999' : '#666666' }]}>
<Text style={[styles.menuYear, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{item.year}
</Text>
)}
@ -206,11 +208,11 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
<MaterialIcons
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
size={24}
color={colors.primary}
color={currentTheme.colors.primary}
/>
<Text style={[
styles.menuOptionText,
{ color: isDarkMode ? '#FFFFFF' : '#000000' }
{ color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }
]}>
{option.label}
</Text>
@ -231,6 +233,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const { currentTheme } = useTheme();
const handleLongPress = useCallback(() => {
setMenuVisible(true);
@ -306,22 +309,22 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
}}
/>
{(!imageLoaded || imageError) && (
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
{!imageError ? (
<ActivityIndicator color={colors.primary} size="small" />
<ActivityIndicator color={currentTheme.colors.primary} size="small" />
) : (
<MaterialIcons name="broken-image" size={24} color={colors.lightGray} />
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.lightGray} />
)}
</View>
)}
{isWatched && (
<View style={styles.watchedIndicator}>
<MaterialIcons name="check-circle" size={22} color={colors.success} />
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
</View>
)}
{localItem.inLibrary && (
<View style={styles.libraryBadge}>
<MaterialIcons name="bookmark" size={16} color={colors.white} />
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
</View>
)}
</View>
@ -344,17 +347,21 @@ const SAMPLE_CATEGORIES: Category[] = [
{ id: 'channel', name: 'Channels' },
];
const SkeletonCatalog = () => (
<View style={styles.catalogContainer}>
<View style={styles.loadingPlaceholder}>
<ActivityIndicator size="small" color={colors.primary} />
const SkeletonCatalog = () => {
const { currentTheme } = useTheme();
return (
<View style={styles.catalogContainer}>
<View style={styles.loadingPlaceholder}>
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View>
</View>
</View>
);
);
};
const HomeScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isDarkMode = useColorScheme() === 'dark';
const { currentTheme } = useTheme();
const continueWatchingRef = useRef<ContinueWatchingRef>(null);
const { settings } = useSettings();
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
@ -436,9 +443,9 @@ const HomeScreen = () => {
// Only run cleanup when component unmounts completely, not on unfocus
return () => {
StatusBar.setTranslucent(false);
StatusBar.setBackgroundColor(colors.darkBackground);
StatusBar.setBackgroundColor(currentTheme.colors.darkBackground);
};
}, []);
}, [currentTheme.colors.darkBackground]);
useEffect(() => {
navigation.addListener('beforeRemove', () => {});
@ -531,22 +538,22 @@ const HomeScreen = () => {
if (isLoading && !isRefreshing) {
return (
<View style={homeStyles.container}>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
barStyle="light-content"
backgroundColor="transparent"
translucent
/>
<View style={homeStyles.loadingMainContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={homeStyles.loadingText}>Loading your content...</Text>
<View style={styles.loadingMainContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading your content...</Text>
</View>
</View>
);
}
return (
<SafeAreaView style={homeStyles.container}>
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
barStyle="light-content"
backgroundColor="transparent"
@ -557,12 +564,12 @@ const HomeScreen = () => {
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={colors.primary}
colors={[colors.primary, colors.secondary]}
tintColor={currentTheme.colors.primary}
colors={[currentTheme.colors.primary, currentTheme.colors.secondary]}
/>
}
contentContainerStyle={[
homeStyles.scrollContent,
styles.scrollContent,
{ paddingTop: Platform.OS === 'ios' ? 39 : 90 }
]}
showsVerticalScrollIndicator={false}
@ -594,17 +601,17 @@ const HomeScreen = () => {
))
) : (
!catalogsLoading && (
<View style={homeStyles.emptyCatalog}>
<MaterialIcons name="movie-filter" size={40} color={colors.textDark} />
<Text style={{ color: colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
No content available
</Text>
<TouchableOpacity
style={homeStyles.addCatalogButton}
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Settings')}
>
<MaterialIcons name="add-circle" size={20} color={colors.white} />
<Text style={homeStyles.addCatalogButtonText}>Add Catalogs</Text>
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
</TouchableOpacity>
</View>
)
@ -620,11 +627,44 @@ const POSTER_WIDTH = (width - 50) / 3;
const styles = StyleSheet.create<any>({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
},
scrollContent: {
paddingBottom: 40,
},
loadingMainContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 40,
},
loadingText: {
marginTop: 12,
fontSize: 14,
},
emptyCatalog: {
padding: 32,
alignItems: 'center',
margin: 16,
borderRadius: 16,
},
addCatalogButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 30,
marginTop: 16,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
},
addCatalogButtonText: {
fontSize: 14,
fontWeight: '600',
marginLeft: 8,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
@ -661,7 +701,6 @@ const styles = StyleSheet.create<any>({
alignSelf: 'center',
},
featuredTitle: {
color: colors.white,
fontSize: 32,
fontWeight: '900',
marginBottom: 0,
@ -679,13 +718,11 @@ const styles = StyleSheet.create<any>({
gap: 4,
},
genreText: {
color: colors.white,
fontSize: 14,
fontWeight: '500',
opacity: 0.9,
},
genreDot: {
color: colors.white,
fontSize: 14,
fontWeight: '500',
opacity: 0.6,
@ -707,7 +744,7 @@ const styles = StyleSheet.create<any>({
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 30,
backgroundColor: colors.white,
backgroundColor: '#FFFFFF',
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
@ -737,18 +774,16 @@ const styles = StyleSheet.create<any>({
flex: null,
},
playButtonText: {
color: colors.black,
color: '#000000',
fontWeight: '600',
marginLeft: 8,
fontSize: 16,
},
myListButtonText: {
color: colors.white,
fontSize: 12,
fontWeight: '500',
},
infoButtonText: {
color: colors.white,
fontSize: 12,
fontWeight: '500',
},
@ -770,7 +805,6 @@ const styles = StyleSheet.create<any>({
catalogTitle: {
fontSize: 18,
fontWeight: '800',
color: colors.highEmphasis,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 6,
@ -786,13 +820,11 @@ const styles = StyleSheet.create<any>({
seeAllButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.elevation1,
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
seeAllText: {
color: colors.primary,
fontSize: 13,
fontWeight: '700',
marginRight: 4,
@ -841,28 +873,18 @@ const styles = StyleSheet.create<any>({
fontWeight: 'bold',
marginLeft: 3,
},
emptyCatalog: {
padding: 32,
alignItems: 'center',
backgroundColor: colors.elevation1,
margin: 16,
borderRadius: 16,
},
skeletonBox: {
backgroundColor: colors.elevation2,
borderRadius: 16,
overflow: 'hidden',
},
skeletonFeatured: {
width: '100%',
height: height * 0.6,
backgroundColor: colors.elevation2,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
marginBottom: 0,
},
skeletonPoster: {
backgroundColor: colors.elevation1,
marginHorizontal: 4,
borderRadius: 16,
},
@ -888,7 +910,6 @@ const styles = StyleSheet.create<any>({
modalOverlay: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: colors.transparentDark,
},
modalOverlayPressable: {
flex: 1,
@ -896,7 +917,6 @@ const styles = StyleSheet.create<any>({
dragHandle: {
width: 40,
height: 4,
backgroundColor: colors.transparentLight,
borderRadius: 2,
alignSelf: 'center',
marginTop: 12,
@ -908,7 +928,7 @@ const styles = StyleSheet.create<any>({
paddingBottom: Platform.select({ ios: 40, android: 24 }),
...Platform.select({
ios: {
shadowColor: colors.black,
shadowColor: '#000',
shadowOffset: { width: 0, height: -3 },
shadowOpacity: 0.1,
shadowRadius: 5,
@ -922,7 +942,6 @@ const styles = StyleSheet.create<any>({
flexDirection: 'row',
padding: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
menuPoster: {
width: 60,
@ -962,7 +981,7 @@ const styles = StyleSheet.create<any>({
position: 'absolute',
top: 8,
right: 8,
backgroundColor: colors.transparentDark,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 12,
padding: 2,
},
@ -970,7 +989,7 @@ const styles = StyleSheet.create<any>({
position: 'absolute',
top: 8,
left: 8,
backgroundColor: colors.transparentDark,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 8,
padding: 4,
},
@ -996,7 +1015,6 @@ const styles = StyleSheet.create<any>({
paddingBottom: 20,
},
featuredTitleText: {
color: colors.highEmphasis,
fontSize: 28,
fontWeight: '900',
marginBottom: 8,
@ -1006,42 +1024,10 @@ const styles = StyleSheet.create<any>({
textAlign: 'center',
paddingHorizontal: 16,
},
addCatalogButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary,
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 30,
marginTop: 16,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
},
addCatalogButtonText: {
color: colors.white,
fontSize: 14,
fontWeight: '600',
marginLeft: 8,
},
loadingMainContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 40,
},
loadingText: {
color: colors.textMuted,
marginTop: 12,
fontSize: 14,
},
loadingPlaceholder: {
height: 200,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.elevation1,
borderRadius: 12,
marginHorizontal: 16,
},
@ -1049,7 +1035,6 @@ const styles = StyleSheet.create<any>({
height: height * 0.4,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.elevation1,
},
});

View file

@ -16,7 +16,6 @@ import {
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../styles';
import { Image } from 'expo-image';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
@ -25,6 +24,7 @@ import type { StreamingContent } from '../services/catalogService';
import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
// Types
interface LibraryItem extends StreamingContent {
@ -38,6 +38,7 @@ const SkeletonLoader = () => {
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
const { width } = useWindowDimensions();
const itemWidth = (width - 48) / 2;
const { currentTheme } = useTheme();
React.useEffect(() => {
const pulse = RNAnimated.loop(
@ -68,13 +69,13 @@ const SkeletonLoader = () => {
<RNAnimated.View
style={[
styles.posterContainer,
{ opacity, backgroundColor: colors.darkBackground }
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]}
/>
<RNAnimated.View
style={[
styles.skeletonTitle,
{ opacity, backgroundColor: colors.darkBackground }
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]}
/>
</View>
@ -99,6 +100,7 @@ const LibraryScreen = () => {
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
const insets = useSafeAreaInsets();
const { currentTheme } = useTheme();
// Force consistent status bar settings
useEffect(() => {
@ -157,7 +159,7 @@ const LibraryScreen = () => {
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
activeOpacity={0.7}
>
<View style={styles.posterContainer}>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
<Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
@ -169,7 +171,7 @@ const LibraryScreen = () => {
style={styles.posterGradient}
>
<Text
style={styles.itemTitle}
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
numberOfLines={2}
>
{item.name}
@ -186,7 +188,7 @@ const LibraryScreen = () => {
<View
style={[
styles.progressBar,
{ width: `${item.progress * 100}%` }
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
]}
/>
</View>
@ -196,10 +198,10 @@ const LibraryScreen = () => {
<MaterialIcons
name="live-tv"
size={14}
color={colors.white}
color={currentTheme.colors.white}
style={{ marginRight: 4 }}
/>
<Text style={styles.badgeText}>Series</Text>
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>Series</Text>
</View>
)}
</View>
@ -212,7 +214,8 @@ const LibraryScreen = () => {
<TouchableOpacity
style={[
styles.filterButton,
isActive && styles.filterButtonActive,
isActive && { backgroundColor: currentTheme.colors.primary },
{ shadowColor: currentTheme.colors.black }
]}
onPress={() => setFilter(filterType)}
activeOpacity={0.7}
@ -220,13 +223,14 @@ const LibraryScreen = () => {
<MaterialIcons
name={iconName}
size={22}
color={isActive ? colors.white : colors.mediumGray}
color={isActive ? currentTheme.colors.white : currentTheme.colors.mediumGray}
style={styles.filterIcon}
/>
<Text
style={[
styles.filterText,
isActive && styles.filterTextActive
{ color: currentTheme.colors.mediumGray },
isActive && { color: currentTheme.colors.white, fontWeight: '600' }
]}
>
{label}
@ -240,20 +244,20 @@ const LibraryScreen = () => {
const headerHeight = headerBaseHeight + topSpacing;
return (
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
{/* Fixed position header background to prevent shifts */}
<View style={[styles.headerBackground, { height: headerHeight }]} />
<View style={[styles.headerBackground, { height: headerHeight, backgroundColor: currentTheme.colors.darkBackground }]} />
<View style={{ flex: 1 }}>
{/* Header Section with proper top spacing */}
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<View style={styles.headerContent}>
<Text style={styles.headerTitle}>Library</Text>
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
</View>
</View>
{/* Content Container */}
<View style={styles.contentContainer}>
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
<View style={styles.filtersContainer}>
{renderFilter('all', 'All', 'apps')}
{renderFilter('movies', 'Movies', 'movie')}
@ -267,19 +271,22 @@ const LibraryScreen = () => {
<MaterialIcons
name="video-library"
size={80}
color={colors.mediumGray}
color={currentTheme.colors.mediumGray}
style={{ opacity: 0.7 }}
/>
<Text style={styles.emptyText}>Your library is empty</Text>
<Text style={styles.emptySubtext}>
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>Your library is empty</Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
Add content to your library to keep track of what you're watching
</Text>
<TouchableOpacity
style={styles.exploreButton}
style={[styles.exploreButton, {
backgroundColor: currentTheme.colors.primary,
shadowColor: currentTheme.colors.black
}]}
onPress={() => navigation.navigate('Discover')}
activeOpacity={0.7}
>
<Text style={styles.exploreButtonText}>Explore Content</Text>
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Explore Content</Text>
</TouchableOpacity>
</View>
) : (
@ -306,19 +313,16 @@ const LibraryScreen = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
},
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.darkBackground,
zIndex: 1,
},
contentContainer: {
flex: 1,
backgroundColor: colors.darkBackground,
},
header: {
paddingHorizontal: 20,
@ -335,7 +339,6 @@ const styles = StyleSheet.create({
headerTitle: {
fontSize: 32,
fontWeight: '800',
color: colors.white,
letterSpacing: 0.3,
},
filtersContainer: {
@ -355,26 +358,17 @@ const styles = StyleSheet.create({
marginHorizontal: 4,
borderRadius: 24,
backgroundColor: 'rgba(255,255,255,0.05)',
shadowColor: colors.black,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
filterButtonActive: {
backgroundColor: colors.primary,
},
filterIcon: {
marginRight: 8,
},
filterText: {
fontSize: 15,
fontWeight: '500',
color: colors.mediumGray,
},
filterTextActive: {
fontWeight: '600',
color: colors.white,
},
listContainer: {
paddingHorizontal: 12,
@ -400,7 +394,6 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(255,255,255,0.03)',
aspectRatio: 2/3,
elevation: 5,
shadowColor: colors.black,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
@ -428,7 +421,6 @@ const styles = StyleSheet.create({
},
progressBar: {
height: '100%',
backgroundColor: colors.primary,
},
badgeContainer: {
position: 'absolute',
@ -442,14 +434,12 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
badgeText: {
color: colors.white,
fontSize: 10,
fontWeight: '600',
},
itemTitle: {
fontSize: 15,
fontWeight: '700',
color: colors.white,
marginBottom: 4,
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: 0, height: 1 },
@ -477,29 +467,24 @@ const styles = StyleSheet.create({
emptyText: {
fontSize: 20,
fontWeight: '700',
color: colors.white,
marginTop: 16,
marginBottom: 8,
},
emptySubtext: {
fontSize: 15,
color: colors.mediumGray,
textAlign: 'center',
marginBottom: 24,
},
exploreButton: {
backgroundColor: colors.primary,
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 24,
elevation: 3,
shadowColor: colors.black,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
exploreButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
}

View file

@ -21,7 +21,6 @@ import {
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../styles';
import { catalogService, StreamingContent } from '../services/catalogService';
import { Image } from 'expo-image';
import debounce from 'lodash/debounce';
@ -42,6 +41,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger';
import { BlurView } from 'expo-blur';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
const { width } = Dimensions.get('window');
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
@ -57,6 +57,7 @@ const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
const SkeletonLoader = () => {
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
const { currentTheme } = useTheme();
React.useEffect(() => {
const pulse = RNAnimated.loop(
@ -84,12 +85,24 @@ const SkeletonLoader = () => {
const renderSkeletonItem = () => (
<View style={styles.skeletonVerticalItem}>
<RNAnimated.View style={[styles.skeletonPoster, { opacity }]} />
<RNAnimated.View style={[
styles.skeletonPoster,
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]} />
<View style={styles.skeletonItemDetails}>
<RNAnimated.View style={[styles.skeletonTitle, { opacity }]} />
<RNAnimated.View style={[
styles.skeletonTitle,
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]} />
<View style={styles.skeletonMetaRow}>
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
<RNAnimated.View style={[
styles.skeletonMeta,
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]} />
<RNAnimated.View style={[
styles.skeletonMeta,
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]} />
</View>
</View>
</View>
@ -100,7 +113,10 @@ const SkeletonLoader = () => {
{[...Array(5)].map((_, index) => (
<View key={index}>
{index === 0 && (
<RNAnimated.View style={[styles.skeletonSectionHeader, { opacity }]} />
<RNAnimated.View style={[
styles.skeletonSectionHeader,
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
]} />
)}
{renderSkeletonItem()}
</View>
@ -116,6 +132,7 @@ const SimpleSearchAnimation = () => {
// Simple animation values that work reliably
const spinAnim = React.useRef(new RNAnimated.Value(0)).current;
const fadeAnim = React.useRef(new RNAnimated.Value(0)).current;
const { currentTheme } = useTheme();
React.useEffect(() => {
// Rotation animation
@ -161,15 +178,15 @@ const SimpleSearchAnimation = () => {
<View style={styles.simpleAnimationContent}>
<RNAnimated.View style={[
styles.spinnerContainer,
{ transform: [{ rotate: spin }] }
{ transform: [{ rotate: spin }], backgroundColor: currentTheme.colors.primary }
]}>
<MaterialIcons
name="search"
size={32}
color={colors.white}
color={currentTheme.colors.white}
/>
</RNAnimated.View>
<Text style={styles.simpleAnimationText}>Searching</Text>
<Text style={[styles.simpleAnimationText, { color: currentTheme.colors.white }]}>Searching</Text>
</View>
</RNAnimated.View>
);
@ -186,6 +203,7 @@ const SearchScreen = () => {
const [showRecent, setShowRecent] = useState(true);
const inputRef = useRef<TextInput>(null);
const insets = useSafeAreaInsets();
const { currentTheme } = useTheme();
// Animation values
const searchBarWidth = useSharedValue(width - 32);
@ -348,7 +366,7 @@ const SearchScreen = () => {
style={styles.recentSearchesContainer}
entering={FadeIn.duration(300)}
>
<Text style={styles.carouselTitle}>
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
Recent Searches
</Text>
{recentSearches.map((search, index) => (
@ -364,10 +382,10 @@ const SearchScreen = () => {
<MaterialIcons
name="history"
size={20}
color={colors.lightGray}
color={currentTheme.colors.lightGray}
style={styles.recentSearchIcon}
/>
<Text style={styles.recentSearchText}>
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
{search}
</Text>
<TouchableOpacity
@ -380,7 +398,7 @@ const SearchScreen = () => {
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.recentSearchDeleteButton}
>
<MaterialIcons name="close" size={16} color={colors.lightGray} />
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
</TouchableOpacity>
</AnimatedTouchable>
))}
@ -398,7 +416,10 @@ const SearchScreen = () => {
entering={FadeIn.duration(500).delay(index * 100)}
activeOpacity={0.7}
>
<View style={styles.horizontalItemPosterContainer}>
<View style={[styles.horizontalItemPosterContainer, {
backgroundColor: currentTheme.colors.darkBackground,
borderColor: 'rgba(255,255,255,0.05)'
}]}>
<Image
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
style={styles.horizontalItemPoster}
@ -406,23 +427,29 @@ const SearchScreen = () => {
transition={300}
/>
<View style={styles.itemTypeContainer}>
<Text style={styles.itemTypeText}>{item.type === 'movie' ? 'MOVIE' : 'SERIES'}</Text>
<Text style={[styles.itemTypeText, { color: currentTheme.colors.white }]}>
{item.type === 'movie' ? 'MOVIE' : 'SERIES'}
</Text>
</View>
{item.imdbRating && (
<View style={styles.ratingContainer}>
<MaterialIcons name="star" size={12} color="#FFC107" />
<Text style={styles.ratingText}>{item.imdbRating}</Text>
<Text style={[styles.ratingText, { color: currentTheme.colors.white }]}>
{item.imdbRating}
</Text>
</View>
)}
</View>
<Text
style={styles.horizontalItemTitle}
style={[styles.horizontalItemTitle, { color: currentTheme.colors.white }]}
numberOfLines={2}
>
{item.name}
</Text>
{item.year && (
<Text style={styles.yearText}>{item.year}</Text>
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray }]}>
{item.year}
</Text>
)}
</AnimatedTouchable>
);
@ -445,7 +472,7 @@ const SearchScreen = () => {
const headerHeight = headerBaseHeight + topSpacing + 60;
return (
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
barStyle="light-content"
backgroundColor="transparent"
@ -453,56 +480,68 @@ const SearchScreen = () => {
/>
{/* Fixed position header background to prevent shifts */}
<View style={[styles.headerBackground, { height: headerHeight }]} />
<View style={[styles.headerBackground, {
height: headerHeight,
backgroundColor: currentTheme.colors.darkBackground
}]} />
<View style={{ flex: 1 }}>
{/* Header Section with proper top spacing */}
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<Text style={styles.headerTitle}>Search</Text>
<View style={[
styles.searchBar,
{
backgroundColor: colors.darkGray,
borderColor: 'transparent',
}
]}>
<MaterialIcons
name="search"
size={24}
color={colors.lightGray}
style={styles.searchIcon}
/>
<TextInput
style={[
styles.searchInput,
{ color: colors.white }
]}
placeholder="Search movies, shows..."
placeholderTextColor={colors.lightGray}
value={query}
onChangeText={setQuery}
returnKeyType="search"
keyboardAppearance="dark"
autoFocus
/>
{query.length > 0 && (
<TouchableOpacity
onPress={handleClearSearch}
style={styles.clearButton}
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Search</Text>
<View style={styles.searchBarContainer}>
<View style={[
styles.searchBarWrapper,
{ width: '100%' }
]}>
<View style={[
styles.searchBar,
{
backgroundColor: currentTheme.colors.elevation2,
borderColor: 'rgba(255,255,255,0.1)',
borderWidth: 1,
}
]}>
<MaterialIcons
name="close"
size={20}
color={colors.lightGray}
name="search"
size={24}
color={currentTheme.colors.lightGray}
style={styles.searchIcon}
/>
</TouchableOpacity>
)}
<TextInput
style={[
styles.searchInput,
{ color: currentTheme.colors.white }
]}
placeholder="Search movies, shows..."
placeholderTextColor={currentTheme.colors.lightGray}
value={query}
onChangeText={setQuery}
returnKeyType="search"
keyboardAppearance="dark"
autoFocus
ref={inputRef}
/>
{query.length > 0 && (
<TouchableOpacity
onPress={handleClearSearch}
style={styles.clearButton}
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<MaterialIcons
name="close"
size={20}
color={currentTheme.colors.lightGray}
/>
</TouchableOpacity>
)}
</View>
</View>
</View>
</View>
{/* Content Container */}
<View style={styles.contentContainer}>
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
{searching ? (
<SimpleSearchAnimation />
) : searched && !hasResultsToShow ? (
@ -513,12 +552,12 @@ const SearchScreen = () => {
<MaterialIcons
name="search-off"
size={64}
color={colors.lightGray}
color={currentTheme.colors.lightGray}
/>
<Text style={styles.emptyText}>
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
No results found
</Text>
<Text style={styles.emptySubtext}>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
Try different keywords or check your spelling
</Text>
</Animated.View>
@ -538,7 +577,9 @@ const SearchScreen = () => {
style={styles.carouselContainer}
entering={FadeIn.duration(300)}
>
<Text style={styles.carouselTitle}>Movies ({movieResults.length})</Text>
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
Movies ({movieResults.length})
</Text>
<FlatList
data={movieResults}
renderItem={renderHorizontalItem}
@ -555,7 +596,9 @@ const SearchScreen = () => {
style={styles.carouselContainer}
entering={FadeIn.duration(300).delay(100)}
>
<Text style={styles.carouselTitle}>TV Shows ({seriesResults.length})</Text>
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
TV Shows ({seriesResults.length})
</Text>
<FlatList
data={seriesResults}
renderItem={renderHorizontalItem}
@ -578,19 +621,16 @@ const SearchScreen = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.black,
},
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.black,
zIndex: 1,
},
contentContainer: {
flex: 1,
backgroundColor: colors.black,
paddingTop: 0,
},
header: {
@ -603,26 +643,26 @@ const styles = StyleSheet.create({
headerTitle: {
fontSize: 32,
fontWeight: '800',
color: colors.white,
letterSpacing: 0.5,
marginBottom: 12,
},
searchBarContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'space-between',
marginBottom: 8,
height: 48,
},
searchBarWrapper: {
flex: 1,
height: 48,
},
searchBar: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
paddingHorizontal: 16,
height: 48,
backgroundColor: colors.darkGray,
height: '100%',
shadowColor: "#000",
shadowOffset: {
width: 0,
@ -632,13 +672,6 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
backButton: {
marginRight: 10,
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
searchIcon: {
marginRight: 12,
},
@ -646,7 +679,6 @@ const styles = StyleSheet.create({
flex: 1,
fontSize: 16,
height: '100%',
color: colors.white,
},
clearButton: {
padding: 4,
@ -664,7 +696,6 @@ const styles = StyleSheet.create({
carouselTitle: {
fontSize: 18,
fontWeight: '700',
color: colors.white,
marginBottom: 12,
paddingHorizontal: 16,
},
@ -681,10 +712,8 @@ const styles = StyleSheet.create({
height: HORIZONTAL_POSTER_HEIGHT,
borderRadius: 12,
overflow: 'hidden',
backgroundColor: colors.darkBackground,
marginBottom: 8,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.05)',
},
horizontalItemPoster: {
width: '100%',
@ -695,11 +724,9 @@ const styles = StyleSheet.create({
fontWeight: '600',
lineHeight: 18,
textAlign: 'left',
color: colors.white,
},
yearText: {
fontSize: 12,
color: colors.mediumGray,
marginTop: 2,
},
recentSearchesContainer: {
@ -723,7 +750,6 @@ const styles = StyleSheet.create({
recentSearchText: {
fontSize: 16,
flex: 1,
color: colors.white,
},
recentSearchDeleteButton: {
padding: 4,
@ -736,7 +762,6 @@ const styles = StyleSheet.create({
loadingText: {
marginTop: 16,
fontSize: 16,
color: colors.white,
},
emptyContainer: {
flex: 1,
@ -749,13 +774,11 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginTop: 16,
marginBottom: 8,
color: colors.white,
},
emptySubtext: {
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
color: colors.lightGray,
},
skeletonContainer: {
flexDirection: 'row',
@ -772,7 +795,6 @@ const styles = StyleSheet.create({
width: POSTER_WIDTH,
height: POSTER_HEIGHT,
borderRadius: 8,
backgroundColor: colors.darkBackground,
},
skeletonItemDetails: {
flex: 1,
@ -788,19 +810,16 @@ const styles = StyleSheet.create({
height: 20,
width: '80%',
marginBottom: 8,
backgroundColor: colors.darkBackground,
borderRadius: 4,
},
skeletonMeta: {
height: 14,
width: '30%',
backgroundColor: colors.darkBackground,
borderRadius: 4,
},
skeletonSectionHeader: {
height: 24,
width: '40%',
backgroundColor: colors.darkBackground,
marginBottom: 16,
borderRadius: 4,
},
@ -814,7 +833,6 @@ const styles = StyleSheet.create({
borderRadius: 4,
},
itemTypeText: {
color: colors.white,
fontSize: 8,
fontWeight: '700',
},
@ -830,7 +848,6 @@ const styles = StyleSheet.create({
borderRadius: 4,
},
ratingText: {
color: colors.white,
fontSize: 10,
fontWeight: '700',
marginLeft: 2,
@ -847,7 +864,6 @@ const styles = StyleSheet.create({
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16,
@ -861,7 +877,6 @@ const styles = StyleSheet.create({
elevation: 3,
},
simpleAnimationText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
},

View file

@ -19,12 +19,12 @@ 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 { useTheme } from '../contexts/ThemeContext';
import { catalogService, DataSource } from '../services/catalogService';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -39,24 +39,28 @@ interface SettingsCardProps {
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 }
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => {
const { currentTheme } = useTheme();
return (
<View style={[styles.cardContainer]}>
{title && (
<Text style={[
styles.cardTitle,
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
{title.toUpperCase()}
</Text>
)}
<View style={[
styles.card,
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
]}>
{title.toUpperCase()}
</Text>
)}
<View style={[
styles.card,
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
]}>
{children}
{children}
</View>
</View>
</View>
);
);
};
interface SettingItemProps {
title: string;
@ -79,6 +83,8 @@ const SettingItem: React.FC<SettingItemProps> = ({
isDarkMode,
badge
}) => {
const { currentTheme } = useTheme();
return (
<TouchableOpacity
activeOpacity={0.7}
@ -93,21 +99,21 @@ const SettingItem: React.FC<SettingItemProps> = ({
styles.settingIconContainer,
{ backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }
]}>
<MaterialIcons name={icon} size={20} color={colors.primary} />
<MaterialIcons name={icon} size={20} color={currentTheme.colors.primary} />
</View>
<View style={styles.settingContent}>
<View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
<Text style={[styles.settingTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
{title}
</Text>
{description && (
<Text style={[styles.settingDescription, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
<Text style={[styles.settingDescription, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{description}
</Text>
)}
</View>
{badge && (
<View style={[styles.badge, { backgroundColor: colors.primary }]}>
<View style={[styles.badge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.badgeText}>{badge}</Text>
</View>
)}
@ -126,6 +132,7 @@ const SettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { lastUpdate } = useCatalogContext();
const { isAuthenticated, userProfile } = useTraktContext();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
// States for dynamic content
@ -229,8 +236,8 @@ const SettingsScreen: React.FC = () => {
<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) : ''}
trackColor={{ false: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? (value ? currentTheme.colors.white : currentTheme.colors.white) : ''}
ios_backgroundColor={isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
/>
);
@ -257,22 +264,22 @@ const SettingsScreen: React.FC = () => {
return (
<View style={[
styles.container,
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
{ backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
]}>
{/* Fixed position header background to prevent shifts */}
<View style={[
styles.headerBackground,
{ height: headerHeight, backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
{ height: headerHeight, backgroundColor: isDarkMode ? currentTheme.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 }]}>
<Text style={[styles.headerTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Settings
</Text>
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
<Text style={[styles.resetButtonText, {color: colors.primary}]}>Reset</Text>
<Text style={[styles.resetButtonText, {color: currentTheme.colors.primary}]}>Reset</Text>
</TouchableOpacity>
</View>
@ -399,25 +406,37 @@ const SettingsScreen: React.FC = () => {
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
discoverDataSource === DataSource.STREMIO_ADDONS && {
backgroundColor: currentTheme.colors.primary
}
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
{ color: currentTheme.colors.mediumEmphasis },
discoverDataSource === DataSource.STREMIO_ADDONS && {
color: currentTheme.colors.white,
fontWeight: '600'
}
]}>Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
discoverDataSource === DataSource.TMDB && {
backgroundColor: currentTheme.colors.primary
}
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
{ color: currentTheme.colors.mediumEmphasis },
discoverDataSource === DataSource.TMDB && {
color: currentTheme.colors.white,
fontWeight: '600'
}
]}>TMDB</Text>
</TouchableOpacity>
</View>
@ -425,8 +444,32 @@ const SettingsScreen: React.FC = () => {
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Appearance">
<SettingItem
title="Dark Mode"
description="Enable dark mode for the app"
icon="brightness-6"
renderControl={() => (
<CustomSwitch
value={settings.enableDarkMode}
onValueChange={(value) => updateSetting('enableDarkMode', value)}
/>
)}
isDarkMode={isDarkMode}
/>
<SettingItem
title="Themes"
description="Customize app colors and themes"
icon="palette"
renderControl={ChevronRight}
onPress={() => navigation.navigate('ThemeSettings')}
isDarkMode={isDarkMode}
isLast
/>
</SettingsCard>
<View style={styles.versionContainer}>
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
<Text style={[styles.versionText, {color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark}]}>
Version 1.0.0
</Text>
</View>
@ -597,17 +640,9 @@ const styles = StyleSheet.create({
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',
},
});

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
View,
Text,
@ -15,12 +15,17 @@ import {
Keyboard,
Clipboard,
Switch,
Image,
KeyboardAvoidingView,
TouchableWithoutFeedback,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors } from '../styles/colors';
import { tmdbService } from '../services/tmdbService';
import { useSettings } from '../hooks/useSettings';
import { logger } from '../utils/logger';
import { useTheme } from '../contexts/ThemeContext';
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
@ -35,6 +40,7 @@ const TMDBSettingsScreen = () => {
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const [isInputFocused, setIsInputFocused] = useState(false);
const apiKeyInputRef = useRef<TextInput>(null);
const { currentTheme } = useTheme();
useEffect(() => {
logger.log('[TMDBSettingsScreen] Component mounted');
@ -217,12 +223,231 @@ const TMDBSettingsScreen = () => {
});
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: currentTheme.colors.darkBackground,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: currentTheme.colors.white,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
paddingHorizontal: 16,
paddingBottom: 16,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
},
backText: {
color: currentTheme.colors.primary,
fontSize: 16,
fontWeight: '500',
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingBottom: 40,
},
titleContainer: {
paddingTop: 8,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: currentTheme.colors.white,
marginHorizontal: 16,
marginBottom: 16,
},
switchCard: {
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
switchTextContainer: {
flex: 1,
marginRight: 12,
},
switchTitle: {
fontSize: 16,
fontWeight: '500',
color: currentTheme.colors.white,
},
switchDescription: {
fontSize: 14,
color: currentTheme.colors.mediumEmphasis,
lineHeight: 20,
},
statusCard: {
flexDirection: 'row',
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
},
statusIconContainer: {
marginRight: 12,
},
statusTextContainer: {
flex: 1,
},
statusTitle: {
fontSize: 16,
fontWeight: '500',
color: currentTheme.colors.white,
marginBottom: 4,
},
statusDescription: {
fontSize: 14,
color: currentTheme.colors.mediumEmphasis,
},
card: {
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
},
cardTitle: {
fontSize: 16,
fontWeight: '500',
color: currentTheme.colors.white,
marginBottom: 16,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
input: {
flex: 1,
backgroundColor: currentTheme.colors.elevation1,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
color: currentTheme.colors.white,
fontSize: 15,
borderWidth: 1,
borderColor: 'transparent',
},
inputFocused: {
borderColor: currentTheme.colors.primary,
},
pasteButton: {
position: 'absolute',
right: 8,
padding: 4,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
button: {
backgroundColor: currentTheme.colors.primary,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
alignItems: 'center',
flex: 1,
marginRight: 8,
},
clearButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: currentTheme.colors.error,
marginRight: 0,
marginLeft: 8,
flex: 0,
},
buttonText: {
color: currentTheme.colors.white,
fontWeight: '500',
fontSize: 15,
},
clearButtonText: {
color: currentTheme.colors.error,
},
resultMessage: {
borderRadius: 8,
padding: 12,
marginTop: 16,
flexDirection: 'row',
alignItems: 'center',
},
successMessage: {
backgroundColor: currentTheme.colors.success + '1A', // 10% opacity
},
errorMessage: {
backgroundColor: currentTheme.colors.error + '1A', // 10% opacity
},
resultIcon: {
marginRight: 8,
},
resultText: {
flex: 1,
},
successText: {
color: currentTheme.colors.success,
},
errorText: {
color: currentTheme.colors.error,
},
helpLink: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 8,
},
helpIcon: {
marginRight: 4,
},
helpText: {
color: currentTheme.colors.primary,
fontSize: 14,
},
infoCard: {
backgroundColor: currentTheme.colors.elevation1,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
flexDirection: 'row',
alignItems: 'flex-start',
},
infoIcon: {
marginRight: 8,
marginTop: 2,
},
infoText: {
color: currentTheme.colors.mediumEmphasis,
fontSize: 14,
flex: 1,
lineHeight: 20,
},
});
if (isLoading) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={styles.loadingText}>Loading Settings...</Text>
</View>
</SafeAreaView>
@ -237,42 +462,43 @@ const TMDBSettingsScreen = () => {
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
<Text style={styles.backText}>Settings</Text>
</TouchableOpacity>
</View>
<Text style={styles.headerTitle}>TMDb Settings</Text>
<Text style={styles.title}>TMDb Settings</Text>
<ScrollView
style={styles.content}
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<View style={styles.switchCard}>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>Use Custom TMDb API Key</Text>
<Switch
value={useCustomKey}
onValueChange={toggleUseCustomKey}
trackColor={{ false: colors.lightGray, true: colors.accentLight }}
thumbColor={Platform.OS === 'android' ? colors.primary : ''}
ios_backgroundColor={colors.lightGray}
/>
<View style={styles.switchTextContainer}>
<Text style={styles.switchTitle}>Use Custom TMDb API Key</Text>
</View>
<Text style={styles.switchDescription}>
Enable to use your own TMDb API key instead of the built-in one.
Using your own API key may provide better performance and higher rate limits.
</Text>
<Switch
value={useCustomKey}
onValueChange={toggleUseCustomKey}
trackColor={{ false: currentTheme.colors.lightGray, true: currentTheme.colors.accentLight }}
thumbColor={Platform.OS === 'android' ? currentTheme.colors.primary : ''}
ios_backgroundColor={currentTheme.colors.lightGray}
/>
</View>
<Text style={styles.switchDescription}>
Enable to use your own TMDb API key instead of the built-in one.
Using your own API key may provide better performance and higher rate limits.
</Text>
{useCustomKey && (
<>
<View style={styles.statusCard}>
<MaterialIcons
name={isKeySet ? "check-circle" : "error-outline"}
size={28}
color={isKeySet ? colors.success : colors.warning}
style={styles.statusIcon}
color={isKeySet ? currentTheme.colors.success : currentTheme.colors.warning}
style={styles.statusIconContainer}
/>
<View style={styles.statusTextContainer}>
<Text style={styles.statusTitle}>
@ -287,8 +513,8 @@ const TMDBSettingsScreen = () => {
</View>
<View style={styles.card}>
<Text style={styles.sectionTitle}>API Key</Text>
<View style={styles.inputWrapper}>
<Text style={styles.cardTitle}>API Key</Text>
<View style={styles.inputContainer}>
<TextInput
ref={apiKeyInputRef}
style={[styles.input, isInputFocused && styles.inputFocused]}
@ -298,7 +524,7 @@ const TMDBSettingsScreen = () => {
if (testResult) setTestResult(null);
}}
placeholder="Paste your TMDb API key (v4 auth)"
placeholderTextColor={colors.mediumGray}
placeholderTextColor={currentTheme.colors.mediumGray}
autoCapitalize="none"
autoCorrect={false}
spellCheck={false}
@ -309,7 +535,7 @@ const TMDBSettingsScreen = () => {
style={styles.pasteButton}
onPress={pasteFromClipboard}
>
<MaterialIcons name="content-paste" size={20} color={colors.primary} />
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
</TouchableOpacity>
</View>
@ -339,7 +565,7 @@ const TMDBSettingsScreen = () => {
<MaterialIcons
name={testResult.success ? "check-circle" : "error"}
size={18}
color={testResult.success ? colors.success : colors.error}
color={testResult.success ? currentTheme.colors.success : currentTheme.colors.error}
style={styles.resultIcon}
/>
<Text style={[
@ -355,7 +581,7 @@ const TMDBSettingsScreen = () => {
style={styles.helpLink}
onPress={openTMDBWebsite}
>
<MaterialIcons name="help" size={16} color={colors.primary} style={styles.helpIcon} />
<MaterialIcons name="help" size={16} color={currentTheme.colors.primary} style={styles.helpIcon} />
<Text style={styles.helpText}>
How to get a TMDb API key?
</Text>
@ -363,7 +589,7 @@ const TMDBSettingsScreen = () => {
</View>
<View style={styles.infoCard}>
<MaterialIcons name="info-outline" size={22} color={colors.primary} style={styles.infoIcon} />
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
<Text style={styles.infoText}>
To get your own TMDb API key (v4 auth token), you need to create a TMDb account and request an API key from their website.
Using your own API key gives you dedicated quota and may improve app performance.
@ -374,7 +600,7 @@ const TMDBSettingsScreen = () => {
{!useCustomKey && (
<View style={styles.infoCard}>
<MaterialIcons name="info-outline" size={22} color={colors.primary} style={styles.infoIcon} />
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
<Text style={styles.infoText}>
Currently using the built-in TMDb API key. This key is shared among all users.
For better performance and reliability, consider using your own API key.
@ -386,236 +612,4 @@ const TMDBSettingsScreen = () => {
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: colors.white,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
paddingBottom: 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
},
backText: {
color: colors.primary,
fontSize: 16,
fontWeight: '500',
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: colors.white,
marginHorizontal: 16,
marginBottom: 16,
},
content: {
flex: 1,
},
scrollContent: {
paddingBottom: 40,
},
switchCard: {
backgroundColor: colors.elevation2,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
switchRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
switchLabel: {
fontSize: 16,
fontWeight: '500',
color: colors.white,
},
switchDescription: {
fontSize: 14,
color: colors.mediumEmphasis,
lineHeight: 20,
},
statusCard: {
flexDirection: 'row',
backgroundColor: colors.elevation2,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
statusIcon: {
marginRight: 12,
},
statusTextContainer: {
flex: 1,
},
statusTitle: {
fontSize: 16,
fontWeight: '500',
color: colors.white,
marginBottom: 4,
},
statusDescription: {
fontSize: 14,
color: colors.mediumEmphasis,
},
card: {
backgroundColor: colors.elevation2,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
sectionTitle: {
fontSize: 16,
fontWeight: '500',
color: colors.white,
marginBottom: 16,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
input: {
flex: 1,
backgroundColor: colors.elevation1,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
color: colors.white,
fontSize: 15,
borderWidth: 1,
borderColor: 'transparent',
},
inputFocused: {
borderColor: colors.primary,
},
pasteButton: {
position: 'absolute',
right: 8,
padding: 8,
},
buttonRow: {
flexDirection: 'row',
marginBottom: 16,
},
button: {
backgroundColor: colors.primary,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 20,
alignItems: 'center',
flex: 1,
marginRight: 8,
},
clearButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: colors.error,
marginRight: 0,
marginLeft: 8,
flex: 0,
},
buttonText: {
color: colors.white,
fontWeight: '500',
fontSize: 15,
},
clearButtonText: {
color: colors.error,
},
resultMessage: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 8,
padding: 12,
marginBottom: 16,
},
successMessage: {
backgroundColor: colors.success + '1A', // 10% opacity
},
errorMessage: {
backgroundColor: colors.error + '1A', // 10% opacity
},
resultIcon: {
marginRight: 8,
},
resultText: {
fontSize: 14,
flex: 1,
},
successText: {
color: colors.success,
},
errorText: {
color: colors.error,
},
helpLink: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 8,
},
helpIcon: {
marginRight: 6,
},
helpText: {
color: colors.primary,
fontSize: 14,
},
infoCard: {
backgroundColor: colors.elevation1,
borderRadius: 12,
marginHorizontal: 16,
marginBottom: 16,
padding: 16,
flexDirection: 'row',
alignItems: 'flex-start',
},
infoIcon: {
marginRight: 12,
marginTop: 2,
},
infoText: {
color: colors.mediumEmphasis,
fontSize: 14,
flex: 1,
lineHeight: 20,
},
});
export default TMDBSettingsScreen;

569
src/screens/ThemeScreen.tsx Normal file
View file

@ -0,0 +1,569 @@
import React, { useState, useCallback, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Switch,
ScrollView,
Alert,
Platform,
TextInput,
Dimensions,
StatusBar,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import ColorPicker from 'react-native-wheel-color-picker';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors } from '../styles/colors';
import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
import { RootStackParamList } from '../navigation/AppNavigator';
const { width } = Dimensions.get('window');
interface ThemeCardProps {
theme: Theme;
isSelected: boolean;
onSelect: () => void;
onEdit?: () => void;
onDelete?: () => void;
}
const ThemeCard: React.FC<ThemeCardProps> = ({
theme,
isSelected,
onSelect,
onEdit,
onDelete
}) => {
return (
<TouchableOpacity
style={[
styles.themeCard,
isSelected && styles.selectedThemeCard,
{ borderColor: theme.colors.primary }
]}
onPress={onSelect}
activeOpacity={0.7}
>
<View style={styles.themeCardHeader}>
<Text style={[styles.themeCardTitle, { color: theme.colors.text }]}>
{theme.name}
</Text>
{isSelected && (
<MaterialIcons name="check-circle" size={20} color={theme.colors.primary} />
)}
</View>
<View style={styles.colorPreviewContainer}>
<View style={[styles.colorPreview, { backgroundColor: theme.colors.primary }]}>
<Text style={styles.colorPreviewLabel}>Primary</Text>
</View>
<View style={[styles.colorPreview, { backgroundColor: theme.colors.secondary }]}>
<Text style={styles.colorPreviewLabel}>Secondary</Text>
</View>
<View style={[styles.colorPreview, { backgroundColor: theme.colors.darkBackground }]}>
<Text style={styles.colorPreviewLabel}>Background</Text>
</View>
</View>
{theme.isEditable && (
<View style={styles.themeCardActions}>
{onEdit && (
<TouchableOpacity style={styles.themeCardAction} onPress={onEdit}>
<MaterialIcons name="edit" size={18} color={theme.colors.primary} />
<Text style={[styles.themeCardActionText, { color: theme.colors.primary }]}>Edit</Text>
</TouchableOpacity>
)}
{onDelete && (
<TouchableOpacity style={styles.themeCardAction} onPress={onDelete}>
<MaterialIcons name="delete" size={18} color={theme.colors.error} />
<Text style={[styles.themeCardActionText, { color: theme.colors.error }]}>Delete</Text>
</TouchableOpacity>
)}
</View>
)}
</TouchableOpacity>
);
};
type ColorKey = 'primary' | 'secondary' | 'darkBackground';
interface ThemeColorEditorProps {
initialColors: {
primary: string;
secondary: string;
darkBackground: string;
};
onSave: (colors: {
primary: string;
secondary: string;
darkBackground: string;
name: string;
}) => void;
onCancel: () => void;
}
const ThemeColorEditor: React.FC<ThemeColorEditorProps> = ({
initialColors,
onSave,
onCancel
}) => {
const [themeName, setThemeName] = useState('Custom Theme');
const [selectedColorKey, setSelectedColorKey] = useState<ColorKey>('primary');
const [themeColors, setThemeColors] = useState({
primary: initialColors.primary,
secondary: initialColors.secondary,
darkBackground: initialColors.darkBackground,
});
const handleColorChange = useCallback((color: string) => {
setThemeColors(prev => ({
...prev,
[selectedColorKey]: color,
}));
}, [selectedColorKey]);
const handleSave = () => {
if (!themeName.trim()) {
Alert.alert('Invalid Name', 'Please enter a valid theme name');
return;
}
onSave({
...themeColors,
name: themeName
});
};
return (
<View style={styles.editorContainer}>
<Text style={styles.editorTitle}>Custom Theme</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Theme Name</Text>
<TextInput
style={styles.textInput}
value={themeName}
onChangeText={setThemeName}
placeholder="Enter theme name"
placeholderTextColor="rgba(255,255,255,0.5)"
/>
</View>
<View style={styles.colorSelectorContainer}>
<TouchableOpacity
style={[
styles.colorSelectorButton,
selectedColorKey === 'primary' && styles.selectedColorButton,
{ backgroundColor: themeColors.primary }
]}
onPress={() => setSelectedColorKey('primary')}
>
<Text style={styles.colorButtonText}>Primary</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.colorSelectorButton,
selectedColorKey === 'secondary' && styles.selectedColorButton,
{ backgroundColor: themeColors.secondary }
]}
onPress={() => setSelectedColorKey('secondary')}
>
<Text style={styles.colorButtonText}>Secondary</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.colorSelectorButton,
selectedColorKey === 'darkBackground' && styles.selectedColorButton,
{ backgroundColor: themeColors.darkBackground }
]}
onPress={() => setSelectedColorKey('darkBackground')}
>
<Text style={styles.colorButtonText}>Background</Text>
</TouchableOpacity>
</View>
<View style={styles.colorPickerContainer}>
<ColorPicker
color={themeColors[selectedColorKey]}
onColorChange={handleColorChange}
thumbSize={30}
sliderSize={30}
noSnap={true}
row={false}
/>
</View>
<View style={styles.editorActions}>
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.saveButton} onPress={handleSave}>
<Text style={styles.saveButtonText}>Save Theme</Text>
</TouchableOpacity>
</View>
</View>
);
};
const ThemeScreen: React.FC = () => {
const {
currentTheme,
availableThemes,
setCurrentTheme,
addCustomTheme,
updateCustomTheme,
deleteCustomTheme
} = useTheme();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const insets = useSafeAreaInsets();
const [isEditMode, setIsEditMode] = useState(false);
const [editingTheme, setEditingTheme] = useState<Theme | null>(null);
// Force consistent status bar settings
useEffect(() => {
const applyStatusBarConfig = () => {
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 handleThemeSelect = useCallback((themeId: string) => {
setCurrentTheme(themeId);
}, [setCurrentTheme]);
const handleEditTheme = useCallback((theme: Theme) => {
setEditingTheme(theme);
setIsEditMode(true);
}, []);
const handleDeleteTheme = useCallback((theme: Theme) => {
Alert.alert(
'Delete Theme',
`Are you sure you want to delete "${theme.name}"?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: () => deleteCustomTheme(theme.id)
}
]
);
}, [deleteCustomTheme]);
const handleCreateTheme = useCallback(() => {
setEditingTheme(null);
setIsEditMode(true);
}, []);
const handleSaveTheme = useCallback((themeData: any) => {
if (editingTheme) {
// Update existing theme
updateCustomTheme({
...editingTheme,
name: themeData.name || editingTheme.name,
colors: {
...editingTheme.colors,
primary: themeData.primary,
secondary: themeData.secondary,
darkBackground: themeData.darkBackground,
}
});
} else {
// Create new theme
addCustomTheme({
name: themeData.name || 'Custom Theme',
colors: {
...currentTheme.colors,
primary: themeData.primary,
secondary: themeData.secondary,
darkBackground: themeData.darkBackground,
}
});
}
setIsEditMode(false);
setEditingTheme(null);
}, [editingTheme, updateCustomTheme, addCustomTheme, currentTheme]);
const handleCancelEdit = useCallback(() => {
setIsEditMode(false);
setEditingTheme(null);
}, []);
if (isEditMode) {
const initialColors = editingTheme ? {
primary: editingTheme.colors.primary,
secondary: editingTheme.colors.secondary,
darkBackground: editingTheme.colors.darkBackground,
} : {
primary: currentTheme.colors.primary,
secondary: currentTheme.colors.secondary,
darkBackground: currentTheme.colors.darkBackground,
};
return (
<View style={[
styles.container,
{
backgroundColor: currentTheme.colors.darkBackground,
paddingTop: insets.top,
paddingBottom: insets.bottom,
}
]}>
<ThemeColorEditor
initialColors={initialColors}
onSave={handleSaveTheme}
onCancel={handleCancelEdit}
/>
</View>
);
}
return (
<View style={[
styles.container,
{
backgroundColor: currentTheme.colors.darkBackground,
paddingTop: insets.top,
paddingBottom: insets.bottom,
}
]}>
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>App Themes</Text>
</View>
<ScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted }]}>
SELECT THEME
</Text>
<View style={styles.themeGrid}>
{availableThemes.map(theme => (
<ThemeCard
key={theme.id}
theme={theme}
isSelected={currentTheme.id === theme.id}
onSelect={() => handleThemeSelect(theme.id)}
onEdit={theme.isEditable ? () => handleEditTheme(theme) : undefined}
onDelete={theme.isEditable ? () => handleDeleteTheme(theme) : undefined}
/>
))}
</View>
<TouchableOpacity
style={[styles.createButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={handleCreateTheme}
>
<MaterialIcons name="add" size={24} color="#FFFFFF" />
<Text style={styles.createButtonText}>Create Custom Theme</Text>
</TouchableOpacity>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
},
backButton: {
padding: 8,
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
marginLeft: 16,
},
content: {
flex: 1,
},
contentContainer: {
padding: 16,
},
sectionTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 16,
},
themeGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
themeCard: {
width: (width - 48) / 2,
marginBottom: 16,
borderRadius: 12,
padding: 12,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderWidth: 2,
borderColor: 'transparent',
},
selectedThemeCard: {
borderWidth: 2,
},
themeCardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
themeCardTitle: {
fontSize: 16,
fontWeight: 'bold',
},
colorPreviewContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
colorPreview: {
width: 30,
height: 30,
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
},
colorPreviewLabel: {
fontSize: 6,
color: '#FFFFFF',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
themeCardActions: {
flexDirection: 'row',
justifyContent: 'space-between',
},
themeCardAction: {
flexDirection: 'row',
alignItems: 'center',
padding: 4,
},
themeCardActionText: {
fontSize: 12,
marginLeft: 4,
},
createButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
borderRadius: 12,
marginTop: 16,
},
createButtonText: {
color: '#FFFFFF',
fontWeight: 'bold',
marginLeft: 8,
},
editorContainer: {
flex: 1,
padding: 16,
},
editorTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#FFFFFF',
marginBottom: 24,
},
inputContainer: {
marginBottom: 24,
},
inputLabel: {
fontSize: 14,
color: 'rgba(255,255,255,0.7)',
marginBottom: 8,
},
textInput: {
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 8,
padding: 12,
color: '#FFFFFF',
fontSize: 16,
},
colorSelectorContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 24,
},
colorSelectorButton: {
width: (width - 64) / 3,
padding: 12,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
selectedColorButton: {
borderWidth: 2,
borderColor: '#FFFFFF',
},
colorButtonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: 'bold',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
colorPickerContainer: {
height: 300,
marginBottom: 24,
},
editorActions: {
flexDirection: 'row',
justifyContent: 'space-between',
},
cancelButton: {
width: (width - 48) / 2,
padding: 16,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255,255,255,0.1)',
},
cancelButtonText: {
color: '#FFFFFF',
fontWeight: 'bold',
},
saveButton: {
width: (width - 48) / 2,
padding: 16,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primary,
},
saveButtonText: {
color: '#FFFFFF',
fontWeight: 'bold',
},
});
export default ThemeScreen;

View file

@ -16,10 +16,10 @@ import { useNavigation } from '@react-navigation/native';
import { makeRedirectUri, useAuthRequest, ResponseType, Prompt, CodeChallengeMethod } from 'expo-auth-session';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { traktService, TraktUser } from '../services/traktService';
import { colors } from '../styles/colors';
import { useSettings } from '../hooks/useSettings';
import { logger } from '../utils/logger';
import TraktIcon from '../../assets/rating-icons/trakt.svg';
import { useTheme } from '../contexts/ThemeContext';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -43,6 +43,7 @@ const TraktSettingsScreen: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
const { currentTheme } = useTheme();
const checkAuthStatus = useCallback(async () => {
setIsLoading(true);
@ -151,7 +152,7 @@ const TraktSettingsScreen: React.FC = () => {
return (
<SafeAreaView style={[
styles.container,
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
{ backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
@ -162,12 +163,12 @@ const TraktSettingsScreen: React.FC = () => {
<MaterialIcons
name="arrow-back"
size={24}
color={isDarkMode ? colors.highEmphasis : colors.textDark}
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
/>
</TouchableOpacity>
<Text style={[
styles.headerTitle,
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Trakt Settings
</Text>
@ -179,11 +180,11 @@ const TraktSettingsScreen: React.FC = () => {
>
<View style={[
styles.card,
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
]}>
{isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
</View>
) : isAuthenticated && userProfile ? (
<View style={styles.profileContainer}>
@ -194,7 +195,7 @@ const TraktSettingsScreen: React.FC = () => {
style={styles.avatar}
/>
) : (
<View style={[styles.avatarPlaceholder, { backgroundColor: colors.primary }]}>
<View style={[styles.avatarPlaceholder, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.avatarText}>
{userProfile.name?.charAt(0) || userProfile.username.charAt(0)}
</Text>
@ -203,13 +204,13 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.profileInfo}>
<Text style={[
styles.profileName,
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
{userProfile.name || userProfile.username}
</Text>
<Text style={[
styles.profileUsername,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
@{userProfile.username}
</Text>
@ -224,7 +225,7 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.statsContainer}>
<Text style={[
styles.joinedDate,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Joined {new Date(userProfile.joined_at).toLocaleDateString()}
</Text>
@ -252,20 +253,20 @@ const TraktSettingsScreen: React.FC = () => {
/>
<Text style={[
styles.signInTitle,
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Connect with Trakt
</Text>
<Text style={[
styles.signInDescription,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Sync your watch history, watchlist, and collection with Trakt.tv
</Text>
<TouchableOpacity
style={[
styles.button,
{ backgroundColor: isDarkMode ? colors.primary : colors.primary }
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
]}
onPress={handleSignIn}
disabled={!request || isExchangingCode} // Disable while waiting for response or exchanging code
@ -285,25 +286,25 @@ const TraktSettingsScreen: React.FC = () => {
{isAuthenticated && (
<View style={[
styles.card,
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
]}>
<View style={styles.settingsSection}>
<Text style={[
styles.sectionTitle,
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Sync Settings
</Text>
<View style={styles.settingItem}>
<Text style={[
styles.settingLabel,
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Auto-sync playback progress
</Text>
<Text style={[
styles.settingDescription,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Coming soon
</Text>
@ -311,13 +312,13 @@ const TraktSettingsScreen: React.FC = () => {
<View style={styles.settingItem}>
<Text style={[
styles.settingLabel,
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
]}>
Import watched history
</Text>
<Text style={[
styles.settingDescription,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Coming soon
</Text>
@ -331,7 +332,7 @@ const TraktSettingsScreen: React.FC = () => {
>
<Text style={[
styles.buttonText,
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
]}>
Sync Now (Coming Soon)
</Text>

View file

@ -1,55 +1,40 @@
import { StyleSheet, Dimensions, Platform } from 'react-native';
import { colors } from './colors';
const { width, height } = Dimensions.get('window');
export const POSTER_WIDTH = (width - 50) / 3;
export const POSTER_HEIGHT = POSTER_WIDTH * 1.5;
export const HORIZONTAL_PADDING = 16;
export const homeStyles = StyleSheet.create({
export const sharedStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
},
scrollContent: {
paddingBottom: 40,
section: {
marginBottom: 24,
},
loadingMainContainer: {
flex: 1,
justifyContent: 'center',
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: 40,
marginBottom: 12,
paddingHorizontal: HORIZONTAL_PADDING,
},
loadingText: {
color: colors.textMuted,
marginTop: 12,
fontSize: 14,
sectionTitle: {
fontSize: 18,
fontWeight: '700',
},
emptyCatalog: {
padding: 32,
alignItems: 'center',
backgroundColor: colors.elevation1,
margin: 16,
borderRadius: 16,
},
addCatalogButton: {
seeAllButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary,
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 30,
marginTop: 16,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
},
addCatalogButtonText: {
color: colors.white,
seeAllText: {
fontSize: 14,
fontWeight: '600',
marginLeft: 8,
marginRight: 4,
},
});
export default homeStyles;
export default {
POSTER_WIDTH,
POSTER_HEIGHT,
HORIZONTAL_PADDING,
};

View file

@ -1,25 +1,26 @@
import { StyleSheet, Dimensions } from 'react-native';
import { colors } from '../index';
import { useTheme } from '../../contexts/ThemeContext';
const useDiscoverStyles = () => {
const { width } = Dimensions.get('window');
const { currentTheme } = useTheme();
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
zIndex: 1,
},
contentContainer: {
flex: 1,
backgroundColor: colors.darkBackground,
backgroundColor: currentTheme.colors.darkBackground,
},
header: {
paddingHorizontal: 20,
@ -36,7 +37,7 @@ const useDiscoverStyles = () => {
headerTitle: {
fontSize: 32,
fontWeight: '800',
color: colors.white,
color: currentTheme.colors.white,
letterSpacing: 0.3,
},
searchButton: {
@ -56,7 +57,7 @@ const useDiscoverStyles = () => {
paddingTop: 80,
},
emptyText: {
color: colors.mediumGray,
color: currentTheme.colors.mediumGray,
fontSize: 16,
textAlign: 'center',
paddingHorizontal: 32,