From ed358c85feac614d139560106641c0daaff71ad2 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 3 May 2025 21:49:20 +0530 Subject: [PATCH] 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. --- App.tsx | 62 +- package-lock.json | 16 + package.json | 1 + src/components/NuvioHeader.tsx | 5 +- src/components/discover/CatalogSection.tsx | 16 +- src/components/discover/CategorySelector.tsx | 22 +- src/components/discover/ContentItem.tsx | 9 +- src/components/discover/GenreSelector.tsx | 20 +- src/components/home/ContentItem.tsx | 18 +- src/components/home/FeaturedContent.tsx | 43 +- src/components/home/SkeletonLoaders.tsx | 39 +- src/contexts/ThemeContext.tsx | 233 ++++++++ src/navigation/AppNavigator.tsx | 38 +- src/screens/DiscoverScreen.tsx | 7 +- src/screens/HomeScreen.tsx | 183 +++--- src/screens/LibraryScreen.tsx | 69 +-- src/screens/SearchScreen.tsx | 207 +++---- src/screens/SettingsScreen.tsx | 115 ++-- src/screens/TMDBSettingsScreen.tsx | 516 +++++++++-------- src/screens/ThemeScreen.tsx | 569 +++++++++++++++++++ src/screens/TraktSettingsScreen.tsx | 41 +- src/styles/homeStyles.ts | 57 +- src/styles/screens/discoverStyles.ts | 13 +- 23 files changed, 1577 insertions(+), 722 deletions(-) create mode 100644 src/contexts/ThemeContext.tsx create mode 100644 src/screens/ThemeScreen.tsx diff --git a/App.tsx b/App.tsx index d6d1680..df02cd8 100644 --- a/App.tsx +++ b/App.tsx @@ -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 ( + + + + + + + + + ); +} +function App(): React.JSX.Element { return ( - - - - - - - - + + + diff --git a/package-lock.json b/package-lock.json index 99bfb79..183de35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d78bd87..116472a 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/NuvioHeader.tsx b/src/components/NuvioHeader.tsx index b2478af..d6c10c3 100644 --- a/src/components/NuvioHeader.tsx +++ b/src/components/NuvioHeader.tsx @@ -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; export const NuvioHeader = () => { const navigation = useNavigation(); 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 = () => { diff --git a/src/components/discover/CatalogSection.tsx b/src/components/discover/CatalogSection.tsx index 44bffde..68b0251 100644 --- a/src/components/discover/CatalogSection.tsx +++ b/src/components/discover/CatalogSection.tsx @@ -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>(); + 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) => { - {catalog.genre} - + + {catalog.genre} + + - See All - + See All + @@ -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, }, diff --git a/src/components/discover/CategorySelector.tsx b/src/components/discover/CategorySelector.tsx index a5db821..c090e8a 100644 --- a/src/components/discover/CategorySelector.tsx +++ b/src/components/discover/CategorySelector.tsx @@ -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 = ({ {category.name} ); - }, [selectedCategory, onSelectCategory]); + }, [selectedCategory, onSelectCategory, currentTheme]); return ( @@ -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); \ No newline at end of file diff --git a/src/components/discover/ContentItem.tsx b/src/components/discover/ContentItem.tsx index 263dad9..015db8c 100644 --- a/src/components/discover/ContentItem.tsx +++ b/src/components/discover/ContentItem.tsx @@ -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} > - + { colors={['transparent', 'rgba(0,0,0,0.85)']} style={styles.gradient} > - + {item.name} {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 }, diff --git a/src/components/discover/GenreSelector.tsx b/src/components/discover/GenreSelector.tsx index 7cd4119..7cc4df0 100644 --- a/src/components/discover/GenreSelector.tsx +++ b/src/components/discover/GenreSelector.tsx @@ -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 = ({ {genre} ); - }, [selectedGenre, onSelectGenre]); + }, [selectedGenre, onSelectGenre, currentTheme]); return ( @@ -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); \ No newline at end of file diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 47e810d..c116271 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -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) && ( - + {!imageError ? ( - + ) : ( - + )} )} {isWatched && ( - + )} {localItem.inLibrary && ( - + )} @@ -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, }, diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 3de6723..5da195d 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -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>(); const { settings } = useSettings(); + const { currentTheme } = useTheme(); const [logoUrl, setLogoUrl] = useState(null); const [bannerUrl, setBannerUrl] = useState(null); const prevContentIdRef = useRef(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 /> ) : ( - {featuredContent.name} + + {featuredContent.name} + )} {featuredContent.genres?.slice(0, 3).map((genre, index, array) => ( - {genre} + + {genre} + {index < array.length - 1 && ( - + )} ))} @@ -395,15 +401,15 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat - + {isSaved ? "Saved" : "Save"} { if (featuredContent) { navigation.navigate('Streams', { @@ -413,8 +419,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat } }} > - - Play + + + Play + - - Info + + + Info + @@ -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', }, diff --git a/src/components/home/SkeletonLoaders.tsx b/src/components/home/SkeletonLoaders.tsx index 0127899..1a51a3e 100644 --- a/src/components/home/SkeletonLoaders.tsx +++ b/src/components/home/SkeletonLoaders.tsx @@ -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 = () => ( - - - +export const SkeletonCatalog = () => { + const { currentTheme } = useTheme(); + return ( + + + + - -); + ); +}; -export const SkeletonFeatured = () => ( - - - Loading featured content... - -); +export const SkeletonFeatured = () => { + const { currentTheme } = useTheme(); + return ( + + + Loading featured content... + + ); +}; 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, }, diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..084c3c3 --- /dev/null +++ b/src/contexts/ThemeContext.tsx @@ -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) => void; + updateCustomTheme: (theme: Theme) => void; + deleteCustomTheme: (themeId: string) => void; +} + +// Create the context +const ThemeContext = createContext(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(DEFAULT_THEMES[0]); + const [availableThemes, setAvailableThemes] = useState(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) => { + 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 ( + + {children} + + ); +} + +// 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; +} \ No newline at end of file diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 24abbe9..5e4b70e 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -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; @@ -653,8 +657,7 @@ const MainTabs = () => { // Stack Navigator const AppNavigator = () => { - // Always use dark mode - const isDarkMode = true; + const { currentTheme } = useTheme(); return ( @@ -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, + }, + }} + /> + diff --git a/src/screens/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx index ee6de4f..3df7368 100644 --- a/src/screens/DiscoverScreen.tsx +++ b/src/screens/DiscoverScreen.tsx @@ -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 = () => { @@ -187,7 +188,7 @@ const DiscoverScreen = () => { {/* Content Section */} {loading ? ( - + ) : catalogs.length > 0 ? ( { @@ -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 ( - - - + + + - + {item.name} {item.year && ( - + {item.year} )} @@ -206,11 +208,11 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) {option.label} @@ -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) && ( - + {!imageError ? ( - + ) : ( - + )} )} {isWatched && ( - + )} {localItem.inLibrary && ( - + )} @@ -344,17 +347,21 @@ const SAMPLE_CATEGORIES: Category[] = [ { id: 'channel', name: 'Channels' }, ]; -const SkeletonCatalog = () => ( - - - +const SkeletonCatalog = () => { + const { currentTheme } = useTheme(); + return ( + + + + - -); + ); +}; const HomeScreen = () => { const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; + const { currentTheme } = useTheme(); const continueWatchingRef = useRef(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 ( - + - - - Loading your content... + + + Loading your content... ); } return ( - + { } contentContainerStyle={[ - homeStyles.scrollContent, + styles.scrollContent, { paddingTop: Platform.OS === 'ios' ? 39 : 90 } ]} showsVerticalScrollIndicator={false} @@ -594,17 +601,17 @@ const HomeScreen = () => { )) ) : ( !catalogsLoading && ( - - - + + + No content available navigation.navigate('Settings')} > - - Add Catalogs + + Add Catalogs ) @@ -620,11 +627,44 @@ const POSTER_WIDTH = (width - 50) / 3; const styles = StyleSheet.create({ 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({ alignSelf: 'center', }, featuredTitle: { - color: colors.white, fontSize: 32, fontWeight: '900', marginBottom: 0, @@ -679,13 +718,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, @@ -707,7 +744,7 @@ const styles = StyleSheet.create({ 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({ 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({ catalogTitle: { fontSize: 18, fontWeight: '800', - color: colors.highEmphasis, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6, @@ -786,13 +820,11 @@ const styles = StyleSheet.create({ 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({ 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({ modalOverlay: { flex: 1, justifyContent: 'flex-end', - backgroundColor: colors.transparentDark, }, modalOverlayPressable: { flex: 1, @@ -896,7 +917,6 @@ const styles = StyleSheet.create({ dragHandle: { width: 40, height: 4, - backgroundColor: colors.transparentLight, borderRadius: 2, alignSelf: 'center', marginTop: 12, @@ -908,7 +928,7 @@ const styles = StyleSheet.create({ 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({ flexDirection: 'row', padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: colors.border, }, menuPoster: { width: 60, @@ -962,7 +981,7 @@ const styles = StyleSheet.create({ 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({ 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({ paddingBottom: 20, }, featuredTitleText: { - color: colors.highEmphasis, fontSize: 28, fontWeight: '900', marginBottom: 8, @@ -1006,42 +1024,10 @@ const styles = StyleSheet.create({ 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({ height: height * 0.4, justifyContent: 'center', alignItems: 'center', - backgroundColor: colors.elevation1, }, }); diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index e8b3581..160ac13 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -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 = () => { @@ -99,6 +100,7 @@ const LibraryScreen = () => { const [libraryItems, setLibraryItems] = useState([]); 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} > - + { style={styles.posterGradient} > {item.name} @@ -186,7 +188,7 @@ const LibraryScreen = () => { @@ -196,10 +198,10 @@ const LibraryScreen = () => { - Series + Series )} @@ -212,7 +214,8 @@ const LibraryScreen = () => { setFilter(filterType)} activeOpacity={0.7} @@ -220,13 +223,14 @@ const LibraryScreen = () => { {label} @@ -240,20 +244,20 @@ const LibraryScreen = () => { const headerHeight = headerBaseHeight + topSpacing; return ( - + {/* Fixed position header background to prevent shifts */} - + {/* Header Section with proper top spacing */} - Library + Library {/* Content Container */} - + {renderFilter('all', 'All', 'apps')} {renderFilter('movies', 'Movies', 'movie')} @@ -267,19 +271,22 @@ const LibraryScreen = () => { - Your library is empty - + Your library is empty + Add content to your library to keep track of what you're watching navigation.navigate('Discover')} activeOpacity={0.7} > - Explore Content + Explore Content ) : ( @@ -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', } diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 4e7591c..0e2d471 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -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 = () => ( - + - + - - + + @@ -100,7 +113,10 @@ const SkeletonLoader = () => { {[...Array(5)].map((_, index) => ( {index === 0 && ( - + )} {renderSkeletonItem()} @@ -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 = () => { - Searching + Searching ); @@ -186,6 +203,7 @@ const SearchScreen = () => { const [showRecent, setShowRecent] = useState(true); const inputRef = useRef(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)} > - + Recent Searches {recentSearches.map((search, index) => ( @@ -364,10 +382,10 @@ const SearchScreen = () => { - + {search} { hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} style={styles.recentSearchDeleteButton} > - + ))} @@ -398,7 +416,10 @@ const SearchScreen = () => { entering={FadeIn.duration(500).delay(index * 100)} activeOpacity={0.7} > - + { transition={300} /> - {item.type === 'movie' ? 'MOVIE' : 'SERIES'} + + {item.type === 'movie' ? 'MOVIE' : 'SERIES'} + {item.imdbRating && ( - {item.imdbRating} + + {item.imdbRating} + )} {item.name} {item.year && ( - {item.year} + + {item.year} + )} ); @@ -445,7 +472,7 @@ const SearchScreen = () => { const headerHeight = headerBaseHeight + topSpacing + 60; return ( - + { /> {/* Fixed position header background to prevent shifts */} - + {/* Header Section with proper top spacing */} - Search - - - - {query.length > 0 && ( - + Search + + + - - )} + + {query.length > 0 && ( + + + + )} + + {/* Content Container */} - + {searching ? ( ) : searched && !hasResultsToShow ? ( @@ -513,12 +552,12 @@ const SearchScreen = () => { - + No results found - + Try different keywords or check your spelling @@ -538,7 +577,9 @@ const SearchScreen = () => { style={styles.carouselContainer} entering={FadeIn.duration(300)} > - Movies ({movieResults.length}) + + Movies ({movieResults.length}) + { style={styles.carouselContainer} entering={FadeIn.duration(300).delay(100)} > - TV Shows ({seriesResults.length}) + + TV Shows ({seriesResults.length}) + { 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', }, diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 3330686..a2e739c 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -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 = ({ children, isDarkMode, title }) => ( - - {title && ( - = ({ children, isDarkMode, title }) => { + const { currentTheme } = useTheme(); + + return ( + + {title && ( + + {title.toUpperCase()} + + )} + - {title.toUpperCase()} - - )} - - {children} + {children} + - -); + ); +}; interface SettingItemProps { title: string; @@ -79,6 +83,8 @@ const SettingItem: React.FC = ({ isDarkMode, badge }) => { + const { currentTheme } = useTheme(); + return ( = ({ styles.settingIconContainer, { backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' } ]}> - + - + {title} {description && ( - + {description} )} {badge && ( - + {badge} )} @@ -126,6 +132,7 @@ const SettingsScreen: React.FC = () => { const navigation = useNavigation>(); 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 = () => { ); @@ -257,22 +264,22 @@ const SettingsScreen: React.FC = () => { return ( {/* Fixed position header background to prevent shifts */} {/* Header Section with proper top spacing */} - + Settings - Reset + Reset @@ -399,25 +406,37 @@ const SettingsScreen: React.FC = () => { handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)} > Addons handleDiscoverDataSourceChange(DataSource.TMDB)} > TMDB @@ -425,8 +444,32 @@ const SettingsScreen: React.FC = () => { /> + + ( + updateSetting('enableDarkMode', value)} + /> + )} + isDarkMode={isDarkMode} + /> + navigation.navigate('ThemeSettings')} + isDarkMode={isDarkMode} + isLast + /> + + - + Version 1.0.0 @@ -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', }, }); diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index c07c562..2503999 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -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(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 ( - + Loading Settings... @@ -237,42 +462,43 @@ const TMDBSettingsScreen = () => { style={styles.backButton} onPress={() => navigation.goBack()} > - + Settings - TMDb Settings + TMDb Settings - - Use Custom TMDb API Key - + + Use Custom TMDb API Key - - 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. - + + + 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. + + {useCustomKey && ( <> @@ -287,8 +513,8 @@ const TMDBSettingsScreen = () => { - API Key - + API Key + { 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} > - + @@ -339,7 +565,7 @@ const TMDBSettingsScreen = () => { { style={styles.helpLink} onPress={openTMDBWebsite} > - + How to get a TMDb API key? @@ -363,7 +589,7 @@ const TMDBSettingsScreen = () => { - + 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 && ( - + 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; \ No newline at end of file diff --git a/src/screens/ThemeScreen.tsx b/src/screens/ThemeScreen.tsx new file mode 100644 index 0000000..5305810 --- /dev/null +++ b/src/screens/ThemeScreen.tsx @@ -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 = ({ + theme, + isSelected, + onSelect, + onEdit, + onDelete +}) => { + return ( + + + + {theme.name} + + {isSelected && ( + + )} + + + + + Primary + + + Secondary + + + Background + + + + {theme.isEditable && ( + + {onEdit && ( + + + Edit + + )} + {onDelete && ( + + + Delete + + )} + + )} + + ); +}; + +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 = ({ + initialColors, + onSave, + onCancel +}) => { + const [themeName, setThemeName] = useState('Custom Theme'); + const [selectedColorKey, setSelectedColorKey] = useState('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 ( + + Custom Theme + + + Theme Name + + + + + setSelectedColorKey('primary')} + > + Primary + + + setSelectedColorKey('secondary')} + > + Secondary + + + setSelectedColorKey('darkBackground')} + > + Background + + + + + + + + + + Cancel + + + Save Theme + + + + ); +}; + +const ThemeScreen: React.FC = () => { + const { + currentTheme, + availableThemes, + setCurrentTheme, + addCustomTheme, + updateCustomTheme, + deleteCustomTheme + } = useTheme(); + const navigation = useNavigation>(); + const insets = useSafeAreaInsets(); + + const [isEditMode, setIsEditMode] = useState(false); + const [editingTheme, setEditingTheme] = useState(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 ( + + + + ); + } + + return ( + + + navigation.goBack()} + > + + + App Themes + + + + + SELECT THEME + + + + {availableThemes.map(theme => ( + handleThemeSelect(theme.id)} + onEdit={theme.isEditable ? () => handleEditTheme(theme) : undefined} + onDelete={theme.isEditable ? () => handleDeleteTheme(theme) : undefined} + /> + ))} + + + + + Create Custom Theme + + + + ); +}; + +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; \ No newline at end of file diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index 8c87af7..bf995e8 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -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(null); + const { currentTheme } = useTheme(); const checkAuthStatus = useCallback(async () => { setIsLoading(true); @@ -151,7 +152,7 @@ const TraktSettingsScreen: React.FC = () => { return ( @@ -162,12 +163,12 @@ const TraktSettingsScreen: React.FC = () => { Trakt Settings @@ -179,11 +180,11 @@ const TraktSettingsScreen: React.FC = () => { > {isLoading ? ( - + ) : isAuthenticated && userProfile ? ( @@ -194,7 +195,7 @@ const TraktSettingsScreen: React.FC = () => { style={styles.avatar} /> ) : ( - + {userProfile.name?.charAt(0) || userProfile.username.charAt(0)} @@ -203,13 +204,13 @@ const TraktSettingsScreen: React.FC = () => { {userProfile.name || userProfile.username} @{userProfile.username} @@ -224,7 +225,7 @@ const TraktSettingsScreen: React.FC = () => { Joined {new Date(userProfile.joined_at).toLocaleDateString()} @@ -252,20 +253,20 @@ const TraktSettingsScreen: React.FC = () => { /> Connect with Trakt Sync your watch history, watchlist, and collection with Trakt.tv { {isAuthenticated && ( Sync Settings Auto-sync playback progress Coming soon @@ -311,13 +312,13 @@ const TraktSettingsScreen: React.FC = () => { Import watched history Coming soon @@ -331,7 +332,7 @@ const TraktSettingsScreen: React.FC = () => { > Sync Now (Coming Soon) diff --git a/src/styles/homeStyles.ts b/src/styles/homeStyles.ts index b8b0566..dff5863 100644 --- a/src/styles/homeStyles.ts +++ b/src/styles/homeStyles.ts @@ -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; \ No newline at end of file +export default { + POSTER_WIDTH, + POSTER_HEIGHT, + HORIZONTAL_PADDING, +}; \ No newline at end of file diff --git a/src/styles/screens/discoverStyles.ts b/src/styles/screens/discoverStyles.ts index 1010d2a..18d1bee 100644 --- a/src/styles/screens/discoverStyles.ts +++ b/src/styles/screens/discoverStyles.ts @@ -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,