diff --git a/App.tsx b/App.tsx index d6d168008..df02cd890 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 99bfb79de..183de35fb 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 d78bd87d6..116472a98 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 b2478aff9..d6c10c39c 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 44bffdef7..68b025154 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 a5db821b0..c090e8a7a 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 263dad9c6..015db8c4a 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 7cd41191a..7cc4df083 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 47e810dd4..c116271b2 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 3de67237f..5da195d13 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 01278996e..1a51a3ed4 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 000000000..084c3c328 --- /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 24abbe9a4..5e4b70ea0 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 ee6de4f41..3df736890 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 e8b3581f3..160ac13f5 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 4e7591c3c..0e2d471dc 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 333068610..a2e739cb1 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 c07c562ab..250399904 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 000000000..530581028 --- /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 8c87af764..bf995e88b 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 b8b056601..dff5863ac 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 1010d2af3..18d1bee64 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,