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,