mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Implement theme context integration across components for improved UI consistency
Refactor various components to utilize the new ThemeContext for dynamic theming. This includes updating styles in the App, NuvioHeader, CatalogSection, and other components to reflect the current theme colors. Additionally, introduce a ThemedApp component to centralize theme management and enhance the overall user experience by ensuring consistent styling throughout the application. Update package dependencies to include react-native-wheel-color-picker for enhanced color selection capabilities.
This commit is contained in:
parent
843d31707c
commit
ed358c85fe
23 changed files with 1577 additions and 722 deletions
62
App.tsx
62
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 (
|
||||
<PaperProvider theme={customDarkTheme}>
|
||||
<NavigationContainer
|
||||
theme={customNavigationTheme}
|
||||
// Disable automatic linking which can cause layout issues
|
||||
linking={undefined}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
/>
|
||||
<AppNavigator />
|
||||
</View>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<GenreProvider>
|
||||
<CatalogProvider>
|
||||
<TraktProvider>
|
||||
<PaperProvider theme={CustomDarkTheme}>
|
||||
<NavigationContainer
|
||||
theme={CustomNavigationDarkTheme}
|
||||
// Disable automatic linking which can cause layout issues
|
||||
linking={undefined}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: '#000000' }]}>
|
||||
<StatusBar
|
||||
style="light"
|
||||
/>
|
||||
<AppNavigator />
|
||||
</View>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
<ThemeProvider>
|
||||
<ThemedApp />
|
||||
</ThemeProvider>
|
||||
</TraktProvider>
|
||||
</CatalogProvider>
|
||||
</GenreProvider>
|
||||
|
|
|
|||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import React from 'react';
|
||||
import { View, TouchableOpacity, Platform, StyleSheet, Image } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../styles/colors';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import type { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
|
||||
import Constants, { ExecutionEnvironment } from 'expo-constants';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
export const NuvioHeader = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const route = useRoute();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Only render the header if the current route is 'Home'
|
||||
if (route.name !== 'Home') {
|
||||
|
|
@ -59,7 +60,7 @@ export const NuvioHeader = () => {
|
|||
<MaterialCommunityIcons
|
||||
name="magnify"
|
||||
size={24}
|
||||
color={colors.white}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { View, Text, TouchableOpacity, StyleSheet, FlatList, Dimensions } from '
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { colors } from '../../styles';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { GenreCatalog, Category } from '../../constants/discover';
|
||||
import { StreamingContent } from '../../services/catalogService';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
|
|
@ -16,6 +16,7 @@ interface CatalogSectionProps {
|
|||
|
||||
const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { width } = Dimensions.get('window');
|
||||
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
|
||||
|
||||
|
|
@ -56,16 +57,18 @@ const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
|
|||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{catalog.genre}</Text>
|
||||
<View style={styles.titleBar} />
|
||||
<Text style={[styles.title, { color: currentTheme.colors.white }]}>
|
||||
{catalog.genre}
|
||||
</Text>
|
||||
<View style={[styles.titleBar, { backgroundColor: currentTheme.colors.primary }]} />
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleSeeMorePress}
|
||||
style={styles.seeAllButton}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<Text style={styles.seeAllText}>See All</Text>
|
||||
<MaterialIcons name="arrow-forward-ios" color={colors.primary} size={14} />
|
||||
<Text style={[styles.seeAllText, { color: currentTheme.colors.primary }]}>See All</Text>
|
||||
<MaterialIcons name="arrow-forward-ios" color={currentTheme.colors.primary} size={14} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
|
@ -106,14 +109,12 @@ const styles = StyleSheet.create({
|
|||
titleBar: {
|
||||
width: 32,
|
||||
height: 3,
|
||||
backgroundColor: colors.primary,
|
||||
marginTop: 6,
|
||||
borderRadius: 2,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: colors.white,
|
||||
},
|
||||
seeAllButton: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -123,7 +124,6 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 4,
|
||||
},
|
||||
seeAllText: {
|
||||
color: colors.primary,
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../../styles';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { Category } from '../../constants/discover';
|
||||
|
||||
interface CategorySelectorProps {
|
||||
|
|
@ -15,6 +15,7 @@ const CategorySelector = ({
|
|||
selectedCategory,
|
||||
onSelectCategory
|
||||
}: CategorySelectorProps) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const renderCategoryButton = useCallback((category: Category) => {
|
||||
const isSelected = selectedCategory.id === category.id;
|
||||
|
|
@ -24,7 +25,7 @@ const CategorySelector = ({
|
|||
key={category.id}
|
||||
style={[
|
||||
styles.categoryButton,
|
||||
isSelected && styles.selectedCategoryButton
|
||||
isSelected && { backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={() => onSelectCategory(category)}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -32,19 +33,19 @@ const CategorySelector = ({
|
|||
<MaterialIcons
|
||||
name={category.icon}
|
||||
size={24}
|
||||
color={isSelected ? colors.white : colors.mediumGray}
|
||||
color={isSelected ? currentTheme.colors.white : currentTheme.colors.mediumGray}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.categoryText,
|
||||
isSelected && styles.selectedCategoryText
|
||||
isSelected && { color: currentTheme.colors.white, fontWeight: '700' }
|
||||
]}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, [selectedCategory, onSelectCategory]);
|
||||
}, [selectedCategory, onSelectCategory, currentTheme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
@ -78,24 +79,17 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
maxWidth: 160,
|
||||
justifyContent: 'center',
|
||||
shadowColor: colors.black,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
selectedCategoryButton: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
categoryText: {
|
||||
color: colors.mediumGray,
|
||||
color: '#9e9e9e', // Default medium gray
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
},
|
||||
selectedCategoryText: {
|
||||
color: colors.white,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(CategorySelector);
|
||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { colors } from '../../styles';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { StreamingContent } from '../../services/catalogService';
|
||||
|
||||
interface ContentItemProps {
|
||||
|
|
@ -13,6 +13,7 @@ interface ContentItemProps {
|
|||
|
||||
const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const { currentTheme } = useTheme();
|
||||
const itemWidth = width || (screenWidth - 48) / 2.2; // Default to 2 items per row with spacing
|
||||
|
||||
return (
|
||||
|
|
@ -21,7 +22,7 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
|||
onPress={onPress}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<View style={styles.posterContainer}>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
|
|
@ -33,7 +34,7 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
|||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<Text style={styles.title} numberOfLines={2}>
|
||||
<Text style={[styles.title, { color: currentTheme.colors.white }]} numberOfLines={2}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.year && (
|
||||
|
|
@ -54,7 +55,6 @@ const styles = StyleSheet.create({
|
|||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
elevation: 5,
|
||||
shadowColor: colors.black,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
|
|
@ -75,7 +75,6 @@ const styles = StyleSheet.create({
|
|||
title: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: colors.white,
|
||||
marginBottom: 4,
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
||||
import { colors } from '../../styles';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
||||
interface GenreSelectorProps {
|
||||
genres: string[];
|
||||
|
|
@ -13,6 +13,7 @@ const GenreSelector = ({
|
|||
selectedGenre,
|
||||
onSelectGenre
|
||||
}: GenreSelectorProps) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const renderGenreButton = useCallback((genre: string) => {
|
||||
const isSelected = selectedGenre === genre;
|
||||
|
|
@ -22,7 +23,7 @@ const GenreSelector = ({
|
|||
key={genre}
|
||||
style={[
|
||||
styles.genreButton,
|
||||
isSelected && styles.selectedGenreButton
|
||||
isSelected && { backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={() => onSelectGenre(genre)}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -30,14 +31,14 @@ const GenreSelector = ({
|
|||
<Text
|
||||
style={[
|
||||
styles.genreText,
|
||||
isSelected && styles.selectedGenreText
|
||||
isSelected && { color: currentTheme.colors.white, fontWeight: '600' }
|
||||
]}
|
||||
>
|
||||
{genre}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, [selectedGenre, onSelectGenre]);
|
||||
}, [selectedGenre, onSelectGenre, currentTheme]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
@ -70,25 +71,18 @@ const styles = StyleSheet.create({
|
|||
marginRight: 12,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
shadowColor: colors.black,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
selectedGenreButton: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
genreText: {
|
||||
color: colors.mediumGray,
|
||||
color: '#9e9e9e', // Default medium gray
|
||||
fontWeight: '500',
|
||||
fontSize: 14,
|
||||
},
|
||||
selectedGenreText: {
|
||||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(GenreSelector);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';
|
||||
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform } from 'react-native';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../../styles/colors';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { catalogService, StreamingContent } from '../../services/catalogService';
|
||||
import DropUpMenu from './DropUpMenu';
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
|||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
setMenuVisible(true);
|
||||
|
|
@ -95,22 +96,22 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
|||
}}
|
||||
/>
|
||||
{(!imageLoaded || imageError) && (
|
||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
|
||||
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
{!imageError ? (
|
||||
<ActivityIndicator color={colors.primary} size="small" />
|
||||
<ActivityIndicator color={currentTheme.colors.primary} size="small" />
|
||||
) : (
|
||||
<MaterialIcons name="broken-image" size={24} color={colors.lightGray} />
|
||||
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.lightGray} />
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{isWatched && (
|
||||
<View style={styles.watchedIndicator}>
|
||||
<MaterialIcons name="check-circle" size={22} color={colors.success} />
|
||||
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
|
||||
</View>
|
||||
)}
|
||||
{localItem.inLibrary && (
|
||||
<View style={styles.libraryBadge}>
|
||||
<MaterialIcons name="bookmark" size={16} color={colors.white} />
|
||||
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -160,7 +161,6 @@ const styles = StyleSheet.create({
|
|||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
|
|
@ -169,7 +169,6 @@ const styles = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
backgroundColor: colors.transparentDark,
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
},
|
||||
|
|
@ -177,7 +176,6 @@ const styles = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
backgroundColor: colors.transparentDark,
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
|
|||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../../styles/colors';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
useAnimatedStyle,
|
||||
|
|
@ -32,6 +31,8 @@ import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from
|
|||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { TMDBService } from '../../services/tmdbService';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import type { Theme } from '../../contexts/ThemeContext';
|
||||
|
||||
interface FeaturedContentProps {
|
||||
featuredContent: StreamingContent | null;
|
||||
|
|
@ -47,6 +48,7 @@ const { width, height } = Dimensions.get('window');
|
|||
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { settings } = useSettings();
|
||||
const { currentTheme } = useTheme();
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
|
||||
const prevContentIdRef = useRef<string | null>(null);
|
||||
|
|
@ -350,7 +352,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
'transparent',
|
||||
'rgba(0,0,0,0.1)',
|
||||
'rgba(0,0,0,0.7)',
|
||||
colors.darkBackground,
|
||||
currentTheme.colors.darkBackground,
|
||||
]}
|
||||
locations={[0, 0.3, 0.7, 1]}
|
||||
style={styles.featuredGradient as ViewStyle}
|
||||
|
|
@ -373,14 +375,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
/>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Text style={styles.featuredTitleText as TextStyle}>{featuredContent.name}</Text>
|
||||
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{featuredContent.name}
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.genreContainer as ViewStyle}>
|
||||
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
|
||||
<React.Fragment key={index}>
|
||||
<Text style={styles.genreText as TextStyle}>{genre}</Text>
|
||||
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
{genre}
|
||||
</Text>
|
||||
{index < array.length - 1 && (
|
||||
<Text style={styles.genreDot as TextStyle}>•</Text>
|
||||
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}>•</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
@ -395,15 +401,15 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
<MaterialIcons
|
||||
name={isSaved ? "bookmark" : "bookmark-border"}
|
||||
size={24}
|
||||
color={colors.white}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
<Text style={styles.myListButtonText as TextStyle}>
|
||||
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
{isSaved ? "Saved" : "Save"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.playButton as ViewStyle}
|
||||
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||
onPress={() => {
|
||||
if (featuredContent) {
|
||||
navigation.navigate('Streams', {
|
||||
|
|
@ -413,8 +419,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="play-arrow" size={24} color={colors.black} />
|
||||
<Text style={styles.playButtonText as TextStyle}>Play</Text>
|
||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
Play
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
|
|
@ -428,8 +436,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="info-outline" size={24} color={colors.white} />
|
||||
<Text style={styles.infoButtonText as TextStyle}>Info</Text>
|
||||
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
Info
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</LinearGradient>
|
||||
|
|
@ -446,7 +456,6 @@ const styles = StyleSheet.create({
|
|||
marginTop: 0,
|
||||
marginBottom: 8,
|
||||
position: 'relative',
|
||||
backgroundColor: colors.elevation1,
|
||||
},
|
||||
imageContainer: {
|
||||
width: '100%',
|
||||
|
|
@ -468,7 +477,6 @@ const styles = StyleSheet.create({
|
|||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: colors.elevation1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
|
|
@ -491,7 +499,6 @@ const styles = StyleSheet.create({
|
|||
alignSelf: 'center',
|
||||
},
|
||||
featuredTitleText: {
|
||||
color: colors.highEmphasis,
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
marginBottom: 8,
|
||||
|
|
@ -510,13 +517,11 @@ const styles = StyleSheet.create({
|
|||
gap: 4,
|
||||
},
|
||||
genreText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
opacity: 0.9,
|
||||
},
|
||||
genreDot: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
opacity: 0.6,
|
||||
|
|
@ -538,7 +543,6 @@ const styles = StyleSheet.create({
|
|||
paddingVertical: 14,
|
||||
paddingHorizontal: 32,
|
||||
borderRadius: 30,
|
||||
backgroundColor: colors.white,
|
||||
elevation: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
|
|
@ -568,18 +572,15 @@ const styles = StyleSheet.create({
|
|||
flex: undefined,
|
||||
},
|
||||
playButtonText: {
|
||||
color: colors.black,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
fontSize: 16,
|
||||
},
|
||||
myListButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
infoButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
import React from 'react';
|
||||
import { View, Text, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';
|
||||
import { colors } from '../../styles/colors';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import type { Theme } from '../../contexts/ThemeContext';
|
||||
|
||||
const { height } = Dimensions.get('window');
|
||||
|
||||
export const SkeletonCatalog = () => (
|
||||
<View style={styles.catalogContainer}>
|
||||
<View style={styles.loadingPlaceholder}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
export const SkeletonCatalog = () => {
|
||||
const { currentTheme } = useTheme();
|
||||
return (
|
||||
<View style={styles.catalogContainer}>
|
||||
<View style={[styles.loadingPlaceholder, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export const SkeletonFeatured = () => (
|
||||
<View style={styles.featuredLoadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={styles.loadingText}>Loading featured content...</Text>
|
||||
</View>
|
||||
);
|
||||
export const SkeletonFeatured = () => {
|
||||
const { currentTheme } = useTheme();
|
||||
return (
|
||||
<View style={[styles.featuredLoadingContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading featured content...</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
catalogContainer: {
|
||||
|
|
@ -29,7 +36,6 @@ const styles = StyleSheet.create({
|
|||
height: 200,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
|
|
@ -37,28 +43,23 @@ const styles = StyleSheet.create({
|
|||
height: height * 0.4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
},
|
||||
loadingText: {
|
||||
color: colors.textMuted,
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
},
|
||||
skeletonBox: {
|
||||
backgroundColor: colors.elevation2,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
skeletonFeatured: {
|
||||
width: '100%',
|
||||
height: height * 0.6,
|
||||
backgroundColor: colors.elevation2,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
marginBottom: 0,
|
||||
},
|
||||
skeletonPoster: {
|
||||
backgroundColor: colors.elevation1,
|
||||
marginHorizontal: 4,
|
||||
borderRadius: 16,
|
||||
},
|
||||
|
|
|
|||
233
src/contexts/ThemeContext.tsx
Normal file
233
src/contexts/ThemeContext.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { colors as defaultColors } from '../styles/colors';
|
||||
|
||||
// Define the Theme interface
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
colors: typeof defaultColors;
|
||||
isEditable: boolean;
|
||||
}
|
||||
|
||||
// Default built-in themes
|
||||
export const DEFAULT_THEMES: Theme[] = [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default Dark',
|
||||
colors: defaultColors,
|
||||
isEditable: false,
|
||||
},
|
||||
{
|
||||
id: 'ocean',
|
||||
name: 'Ocean Blue',
|
||||
colors: {
|
||||
...defaultColors,
|
||||
primary: '#3498db',
|
||||
secondary: '#2ecc71',
|
||||
darkBackground: '#0a192f',
|
||||
},
|
||||
isEditable: false,
|
||||
},
|
||||
{
|
||||
id: 'sunset',
|
||||
name: 'Sunset',
|
||||
colors: {
|
||||
...defaultColors,
|
||||
primary: '#ff7e5f',
|
||||
secondary: '#feb47b',
|
||||
darkBackground: '#1a0f0b',
|
||||
},
|
||||
isEditable: false,
|
||||
},
|
||||
{
|
||||
id: 'moonlight',
|
||||
name: 'Moonlight',
|
||||
colors: {
|
||||
...defaultColors,
|
||||
primary: '#a786df',
|
||||
secondary: '#5e72e4',
|
||||
darkBackground: '#0f0f1a',
|
||||
},
|
||||
isEditable: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Theme context props
|
||||
interface ThemeContextProps {
|
||||
currentTheme: Theme;
|
||||
availableThemes: Theme[];
|
||||
setCurrentTheme: (themeId: string) => void;
|
||||
addCustomTheme: (theme: Omit<Theme, 'id' | 'isEditable'>) => void;
|
||||
updateCustomTheme: (theme: Theme) => void;
|
||||
deleteCustomTheme: (themeId: string) => void;
|
||||
}
|
||||
|
||||
// Create the context
|
||||
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
|
||||
|
||||
// Storage keys
|
||||
const CURRENT_THEME_KEY = 'current_theme';
|
||||
const CUSTOM_THEMES_KEY = 'custom_themes';
|
||||
|
||||
// Provider component
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [currentTheme, setCurrentThemeState] = useState<Theme>(DEFAULT_THEMES[0]);
|
||||
const [availableThemes, setAvailableThemes] = useState<Theme[]>(DEFAULT_THEMES);
|
||||
|
||||
// Load themes from AsyncStorage on mount
|
||||
useEffect(() => {
|
||||
const loadThemes = async () => {
|
||||
try {
|
||||
// Load current theme ID
|
||||
const savedThemeId = await AsyncStorage.getItem(CURRENT_THEME_KEY);
|
||||
|
||||
// Load custom themes
|
||||
const customThemesJson = await AsyncStorage.getItem(CUSTOM_THEMES_KEY);
|
||||
const customThemes = customThemesJson ? JSON.parse(customThemesJson) : [];
|
||||
|
||||
// Combine default and custom themes
|
||||
const allThemes = [...DEFAULT_THEMES, ...customThemes];
|
||||
setAvailableThemes(allThemes);
|
||||
|
||||
// Set current theme
|
||||
if (savedThemeId) {
|
||||
const theme = allThemes.find(t => t.id === savedThemeId);
|
||||
if (theme) {
|
||||
setCurrentThemeState(theme);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load themes:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadThemes();
|
||||
}, []);
|
||||
|
||||
// Set current theme
|
||||
const setCurrentTheme = async (themeId: string) => {
|
||||
const theme = availableThemes.find(t => t.id === themeId);
|
||||
if (theme) {
|
||||
setCurrentThemeState(theme);
|
||||
await AsyncStorage.setItem(CURRENT_THEME_KEY, themeId);
|
||||
}
|
||||
};
|
||||
|
||||
// Add custom theme
|
||||
const addCustomTheme = async (themeData: Omit<Theme, 'id' | 'isEditable'>) => {
|
||||
try {
|
||||
// Generate unique ID
|
||||
const id = `custom_${Date.now()}`;
|
||||
|
||||
// Create new theme object
|
||||
const newTheme: Theme = {
|
||||
id,
|
||||
...themeData,
|
||||
isEditable: true,
|
||||
};
|
||||
|
||||
// Add to available themes
|
||||
const customThemes = availableThemes.filter(t => t.isEditable);
|
||||
const updatedCustomThemes = [...customThemes, newTheme];
|
||||
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
|
||||
|
||||
// Save to storage
|
||||
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
|
||||
|
||||
// Update state
|
||||
setAvailableThemes(updatedAllThemes);
|
||||
|
||||
// Set as current theme
|
||||
setCurrentThemeState(newTheme);
|
||||
await AsyncStorage.setItem(CURRENT_THEME_KEY, id);
|
||||
} catch (error) {
|
||||
console.error('Failed to add custom theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update custom theme
|
||||
const updateCustomTheme = async (updatedTheme: Theme) => {
|
||||
try {
|
||||
if (!updatedTheme.isEditable) {
|
||||
throw new Error('Cannot edit built-in themes');
|
||||
}
|
||||
|
||||
// Find and update the theme
|
||||
const customThemes = availableThemes.filter(t => t.isEditable);
|
||||
const updatedCustomThemes = customThemes.map(t =>
|
||||
t.id === updatedTheme.id ? updatedTheme : t
|
||||
);
|
||||
|
||||
// Update available themes
|
||||
const updatedAllThemes = [...DEFAULT_THEMES, ...updatedCustomThemes];
|
||||
|
||||
// Save to storage
|
||||
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updatedCustomThemes));
|
||||
|
||||
// Update state
|
||||
setAvailableThemes(updatedAllThemes);
|
||||
|
||||
// Update current theme if needed
|
||||
if (currentTheme.id === updatedTheme.id) {
|
||||
setCurrentThemeState(updatedTheme);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update custom theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete custom theme
|
||||
const deleteCustomTheme = async (themeId: string) => {
|
||||
try {
|
||||
// Find theme to delete
|
||||
const themeToDelete = availableThemes.find(t => t.id === themeId);
|
||||
|
||||
if (!themeToDelete || !themeToDelete.isEditable) {
|
||||
throw new Error('Cannot delete built-in themes or theme not found');
|
||||
}
|
||||
|
||||
// Filter out the theme
|
||||
const customThemes = availableThemes.filter(t => t.isEditable && t.id !== themeId);
|
||||
const updatedAllThemes = [...DEFAULT_THEMES, ...customThemes];
|
||||
|
||||
// Save to storage
|
||||
await AsyncStorage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(customThemes));
|
||||
|
||||
// Update state
|
||||
setAvailableThemes(updatedAllThemes);
|
||||
|
||||
// Reset to default theme if current theme was deleted
|
||||
if (currentTheme.id === themeId) {
|
||||
setCurrentThemeState(DEFAULT_THEMES[0]);
|
||||
await AsyncStorage.setItem(CURRENT_THEME_KEY, DEFAULT_THEMES[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete custom theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
currentTheme,
|
||||
availableThemes,
|
||||
setCurrentTheme,
|
||||
addCustomTheme,
|
||||
updateCustomTheme,
|
||||
deleteCustomTheme,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook to use the theme context
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import { colors } from '../styles/colors';
|
|||
import { NuvioHeader } from '../components/NuvioHeader';
|
||||
import { Stream } from '../types/streams';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { AnimationFade, AnimationSlideHorizontal } from '../utils/animations';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
// Import screens with their proper types
|
||||
import HomeScreen from '../screens/HomeScreen';
|
||||
|
|
@ -36,6 +38,7 @@ import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
|
|||
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
||||
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
||||
import LogoSourceSettings from '../screens/LogoSourceSettings';
|
||||
import ThemeScreen from '../screens/ThemeScreen';
|
||||
|
||||
// Stack navigator types
|
||||
export type RootStackParamList = {
|
||||
|
|
@ -92,6 +95,7 @@ export type RootStackParamList = {
|
|||
TraktSettings: undefined;
|
||||
PlayerSettings: undefined;
|
||||
LogoSourceSettings: undefined;
|
||||
ThemeSettings: undefined;
|
||||
};
|
||||
|
||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
|
@ -653,8 +657,7 @@ const MainTabs = () => {
|
|||
|
||||
// Stack Navigator
|
||||
const AppNavigator = () => {
|
||||
// Always use dark mode
|
||||
const isDarkMode = true;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
|
|
@ -671,7 +674,7 @@ const AppNavigator = () => {
|
|||
animation: 'none',
|
||||
// Ensure content is not popping in and out
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -723,7 +726,7 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -738,7 +741,7 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -776,7 +779,7 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -791,7 +794,7 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -806,7 +809,7 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -821,7 +824,7 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
@ -836,7 +839,22 @@ const AppNavigator = () => {
|
|||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: colors.darkBackground,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ThemeSettings"
|
||||
component={ThemeScreen}
|
||||
options={{
|
||||
animation: 'fade',
|
||||
animationDuration: 200,
|
||||
presentation: 'card',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import {
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../styles';
|
||||
import { catalogService, StreamingContent } from '../services/catalogService';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
// Components
|
||||
import CategorySelector from '../components/discover/CategorySelector';
|
||||
|
|
@ -37,6 +37,7 @@ const DiscoverScreen = () => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const styles = useDiscoverStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Force consistent status bar settings
|
||||
useEffect(() => {
|
||||
|
|
@ -162,7 +163,7 @@ const DiscoverScreen = () => {
|
|||
<MaterialIcons
|
||||
name="search"
|
||||
size={24}
|
||||
color={colors.white}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -187,7 +188,7 @@ const DiscoverScreen = () => {
|
|||
{/* Content Section */}
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
) : catalogs.length > 0 ? (
|
||||
<CatalogsList
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { Stream } from '../types/metadata';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { colors } from '../styles/colors';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
|
|
@ -58,7 +57,9 @@ import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
|||
import FeaturedContent from '../components/home/FeaturedContent';
|
||||
import CatalogSection from '../components/home/CatalogSection';
|
||||
import { SkeletonFeatured } from '../components/home/SkeletonLoaders';
|
||||
import homeStyles from '../styles/homeStyles';
|
||||
import homeStyles, { sharedStyles } from '../styles/homeStyles';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import type { Theme } from '../contexts/ThemeContext';
|
||||
|
||||
// Define interfaces for our data
|
||||
interface Category {
|
||||
|
|
@ -86,6 +87,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
|||
const translateY = useSharedValue(300);
|
||||
const opacity = useSharedValue(0);
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const { currentTheme } = useTheme();
|
||||
const SNAP_THRESHOLD = 100;
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -126,12 +128,14 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
|||
|
||||
const overlayStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
backgroundColor: currentTheme.colors.transparentDark,
|
||||
}));
|
||||
|
||||
const menuStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: translateY.value }],
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white,
|
||||
}));
|
||||
|
||||
const menuOptions = [
|
||||
|
|
@ -157,8 +161,6 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
|||
}
|
||||
];
|
||||
|
||||
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
|
|
@ -170,20 +172,20 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
|||
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
|
||||
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
|
||||
<GestureDetector gesture={gesture}>
|
||||
<Animated.View style={[styles.menuContainer, menuStyle, { backgroundColor }]}>
|
||||
<View style={styles.dragHandle} />
|
||||
<View style={styles.menuHeader}>
|
||||
<Animated.View style={[styles.menuContainer, menuStyle]}>
|
||||
<View style={[styles.dragHandle, { backgroundColor: currentTheme.colors.transparentLight }]} />
|
||||
<View style={[styles.menuHeader, { borderBottomColor: currentTheme.colors.border }]}>
|
||||
<ExpoImage
|
||||
source={{ uri: item.poster }}
|
||||
style={styles.menuPoster}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<View style={styles.menuTitleContainer}>
|
||||
<Text style={[styles.menuTitle, { color: isDarkMode ? '#FFFFFF' : '#000000' }]}>
|
||||
<Text style={[styles.menuTitle, { color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.year && (
|
||||
<Text style={[styles.menuYear, { color: isDarkMode ? '#999999' : '#666666' }]}>
|
||||
<Text style={[styles.menuYear, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
|
||||
{item.year}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -206,11 +208,11 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
|||
<MaterialIcons
|
||||
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.menuOptionText,
|
||||
{ color: isDarkMode ? '#FFFFFF' : '#000000' }
|
||||
{ color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
|
|
@ -231,6 +233,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
|||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
setMenuVisible(true);
|
||||
|
|
@ -306,22 +309,22 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
|||
}}
|
||||
/>
|
||||
{(!imageLoaded || imageError) && (
|
||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
|
||||
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
{!imageError ? (
|
||||
<ActivityIndicator color={colors.primary} size="small" />
|
||||
<ActivityIndicator color={currentTheme.colors.primary} size="small" />
|
||||
) : (
|
||||
<MaterialIcons name="broken-image" size={24} color={colors.lightGray} />
|
||||
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.lightGray} />
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{isWatched && (
|
||||
<View style={styles.watchedIndicator}>
|
||||
<MaterialIcons name="check-circle" size={22} color={colors.success} />
|
||||
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
|
||||
</View>
|
||||
)}
|
||||
{localItem.inLibrary && (
|
||||
<View style={styles.libraryBadge}>
|
||||
<MaterialIcons name="bookmark" size={16} color={colors.white} />
|
||||
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -344,17 +347,21 @@ const SAMPLE_CATEGORIES: Category[] = [
|
|||
{ id: 'channel', name: 'Channels' },
|
||||
];
|
||||
|
||||
const SkeletonCatalog = () => (
|
||||
<View style={styles.catalogContainer}>
|
||||
<View style={styles.loadingPlaceholder}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
const SkeletonCatalog = () => {
|
||||
const { currentTheme } = useTheme();
|
||||
return (
|
||||
<View style={styles.catalogContainer}>
|
||||
<View style={styles.loadingPlaceholder}>
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const HomeScreen = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const { currentTheme } = useTheme();
|
||||
const continueWatchingRef = useRef<ContinueWatchingRef>(null);
|
||||
const { settings } = useSettings();
|
||||
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
|
||||
|
|
@ -436,9 +443,9 @@ const HomeScreen = () => {
|
|||
// Only run cleanup when component unmounts completely, not on unfocus
|
||||
return () => {
|
||||
StatusBar.setTranslucent(false);
|
||||
StatusBar.setBackgroundColor(colors.darkBackground);
|
||||
StatusBar.setBackgroundColor(currentTheme.colors.darkBackground);
|
||||
};
|
||||
}, []);
|
||||
}, [currentTheme.colors.darkBackground]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.addListener('beforeRemove', () => {});
|
||||
|
|
@ -531,22 +538,22 @@ const HomeScreen = () => {
|
|||
|
||||
if (isLoading && !isRefreshing) {
|
||||
return (
|
||||
<View style={homeStyles.container}>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
<View style={homeStyles.loadingMainContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={homeStyles.loadingText}>Loading your content...</Text>
|
||||
<View style={styles.loadingMainContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading your content...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={homeStyles.container}>
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
backgroundColor="transparent"
|
||||
|
|
@ -557,12 +564,12 @@ const HomeScreen = () => {
|
|||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.primary}
|
||||
colors={[colors.primary, colors.secondary]}
|
||||
tintColor={currentTheme.colors.primary}
|
||||
colors={[currentTheme.colors.primary, currentTheme.colors.secondary]}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={[
|
||||
homeStyles.scrollContent,
|
||||
styles.scrollContent,
|
||||
{ paddingTop: Platform.OS === 'ios' ? 39 : 90 }
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
|
|
@ -594,17 +601,17 @@ const HomeScreen = () => {
|
|||
))
|
||||
) : (
|
||||
!catalogsLoading && (
|
||||
<View style={homeStyles.emptyCatalog}>
|
||||
<MaterialIcons name="movie-filter" size={40} color={colors.textDark} />
|
||||
<Text style={{ color: colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
||||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||
No content available
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={homeStyles.addCatalogButton}
|
||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
>
|
||||
<MaterialIcons name="add-circle" size={20} color={colors.white} />
|
||||
<Text style={homeStyles.addCatalogButtonText}>Add Catalogs</Text>
|
||||
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
|
|
@ -620,11 +627,44 @@ const POSTER_WIDTH = (width - 50) / 3;
|
|||
const styles = StyleSheet.create<any>({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
loadingMainContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 40,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
},
|
||||
emptyCatalog: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
margin: 16,
|
||||
borderRadius: 16,
|
||||
},
|
||||
addCatalogButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 30,
|
||||
marginTop: 16,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
addCatalogButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
|
|
@ -661,7 +701,6 @@ const styles = StyleSheet.create<any>({
|
|||
alignSelf: 'center',
|
||||
},
|
||||
featuredTitle: {
|
||||
color: colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: '900',
|
||||
marginBottom: 0,
|
||||
|
|
@ -679,13 +718,11 @@ const styles = StyleSheet.create<any>({
|
|||
gap: 4,
|
||||
},
|
||||
genreText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
opacity: 0.9,
|
||||
},
|
||||
genreDot: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
opacity: 0.6,
|
||||
|
|
@ -707,7 +744,7 @@ const styles = StyleSheet.create<any>({
|
|||
paddingVertical: 14,
|
||||
paddingHorizontal: 32,
|
||||
borderRadius: 30,
|
||||
backgroundColor: colors.white,
|
||||
backgroundColor: '#FFFFFF',
|
||||
elevation: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
|
|
@ -737,18 +774,16 @@ const styles = StyleSheet.create<any>({
|
|||
flex: null,
|
||||
},
|
||||
playButtonText: {
|
||||
color: colors.black,
|
||||
color: '#000000',
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
fontSize: 16,
|
||||
},
|
||||
myListButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
infoButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
|
@ -770,7 +805,6 @@ const styles = StyleSheet.create<any>({
|
|||
catalogTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
color: colors.highEmphasis,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 6,
|
||||
|
|
@ -786,13 +820,11 @@ const styles = StyleSheet.create<any>({
|
|||
seeAllButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
},
|
||||
seeAllText: {
|
||||
color: colors.primary,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
marginRight: 4,
|
||||
|
|
@ -841,28 +873,18 @@ const styles = StyleSheet.create<any>({
|
|||
fontWeight: 'bold',
|
||||
marginLeft: 3,
|
||||
},
|
||||
emptyCatalog: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
margin: 16,
|
||||
borderRadius: 16,
|
||||
},
|
||||
skeletonBox: {
|
||||
backgroundColor: colors.elevation2,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
skeletonFeatured: {
|
||||
width: '100%',
|
||||
height: height * 0.6,
|
||||
backgroundColor: colors.elevation2,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
marginBottom: 0,
|
||||
},
|
||||
skeletonPoster: {
|
||||
backgroundColor: colors.elevation1,
|
||||
marginHorizontal: 4,
|
||||
borderRadius: 16,
|
||||
},
|
||||
|
|
@ -888,7 +910,6 @@ const styles = StyleSheet.create<any>({
|
|||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: colors.transparentDark,
|
||||
},
|
||||
modalOverlayPressable: {
|
||||
flex: 1,
|
||||
|
|
@ -896,7 +917,6 @@ const styles = StyleSheet.create<any>({
|
|||
dragHandle: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: colors.transparentLight,
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginTop: 12,
|
||||
|
|
@ -908,7 +928,7 @@ const styles = StyleSheet.create<any>({
|
|||
paddingBottom: Platform.select({ ios: 40, android: 24 }),
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: colors.black,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: -3 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 5,
|
||||
|
|
@ -922,7 +942,6 @@ const styles = StyleSheet.create<any>({
|
|||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
menuPoster: {
|
||||
width: 60,
|
||||
|
|
@ -962,7 +981,7 @@ const styles = StyleSheet.create<any>({
|
|||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
backgroundColor: colors.transparentDark,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
},
|
||||
|
|
@ -970,7 +989,7 @@ const styles = StyleSheet.create<any>({
|
|||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
backgroundColor: colors.transparentDark,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
},
|
||||
|
|
@ -996,7 +1015,6 @@ const styles = StyleSheet.create<any>({
|
|||
paddingBottom: 20,
|
||||
},
|
||||
featuredTitleText: {
|
||||
color: colors.highEmphasis,
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
marginBottom: 8,
|
||||
|
|
@ -1006,42 +1024,10 @@ const styles = StyleSheet.create<any>({
|
|||
textAlign: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
addCatalogButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 30,
|
||||
marginTop: 16,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
addCatalogButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
},
|
||||
loadingMainContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 40,
|
||||
},
|
||||
loadingText: {
|
||||
color: colors.textMuted,
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
},
|
||||
loadingPlaceholder: {
|
||||
height: 200,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
|
|
@ -1049,7 +1035,6 @@ const styles = StyleSheet.create<any>({
|
|||
height: height * 0.4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../styles';
|
||||
import { Image } from 'expo-image';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
|
|
@ -25,6 +24,7 @@ import type { StreamingContent } from '../services/catalogService';
|
|||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
// Types
|
||||
interface LibraryItem extends StreamingContent {
|
||||
|
|
@ -38,6 +38,7 @@ const SkeletonLoader = () => {
|
|||
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||
const { width } = useWindowDimensions();
|
||||
const itemWidth = (width - 48) / 2;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
const pulse = RNAnimated.loop(
|
||||
|
|
@ -68,13 +69,13 @@ const SkeletonLoader = () => {
|
|||
<RNAnimated.View
|
||||
style={[
|
||||
styles.posterContainer,
|
||||
{ opacity, backgroundColor: colors.darkBackground }
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]}
|
||||
/>
|
||||
<RNAnimated.View
|
||||
style={[
|
||||
styles.skeletonTitle,
|
||||
{ opacity, backgroundColor: colors.darkBackground }
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -99,6 +100,7 @@ const LibraryScreen = () => {
|
|||
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
||||
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Force consistent status bar settings
|
||||
useEffect(() => {
|
||||
|
|
@ -157,7 +159,7 @@ const LibraryScreen = () => {
|
|||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.posterContainer}>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
|
|
@ -169,7 +171,7 @@ const LibraryScreen = () => {
|
|||
style={styles.posterGradient}
|
||||
>
|
||||
<Text
|
||||
style={styles.itemTitle}
|
||||
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
|
|
@ -186,7 +188,7 @@ const LibraryScreen = () => {
|
|||
<View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${item.progress * 100}%` }
|
||||
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -196,10 +198,10 @@ const LibraryScreen = () => {
|
|||
<MaterialIcons
|
||||
name="live-tv"
|
||||
size={14}
|
||||
color={colors.white}
|
||||
color={currentTheme.colors.white}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text style={styles.badgeText}>Series</Text>
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>Series</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -212,7 +214,8 @@ const LibraryScreen = () => {
|
|||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterButton,
|
||||
isActive && styles.filterButtonActive,
|
||||
isActive && { backgroundColor: currentTheme.colors.primary },
|
||||
{ shadowColor: currentTheme.colors.black }
|
||||
]}
|
||||
onPress={() => setFilter(filterType)}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -220,13 +223,14 @@ const LibraryScreen = () => {
|
|||
<MaterialIcons
|
||||
name={iconName}
|
||||
size={22}
|
||||
color={isActive ? colors.white : colors.mediumGray}
|
||||
color={isActive ? currentTheme.colors.white : currentTheme.colors.mediumGray}
|
||||
style={styles.filterIcon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.filterText,
|
||||
isActive && styles.filterTextActive
|
||||
{ color: currentTheme.colors.mediumGray },
|
||||
isActive && { color: currentTheme.colors.white, fontWeight: '600' }
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
|
|
@ -240,20 +244,20 @@ const LibraryScreen = () => {
|
|||
const headerHeight = headerBaseHeight + topSpacing;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
{/* Fixed position header background to prevent shifts */}
|
||||
<View style={[styles.headerBackground, { height: headerHeight }]} />
|
||||
<View style={[styles.headerBackground, { height: headerHeight, backgroundColor: currentTheme.colors.darkBackground }]} />
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Header Section with proper top spacing */}
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
<View style={styles.headerContent}>
|
||||
<Text style={styles.headerTitle}>Library</Text>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content Container */}
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<View style={styles.filtersContainer}>
|
||||
{renderFilter('all', 'All', 'apps')}
|
||||
{renderFilter('movies', 'Movies', 'movie')}
|
||||
|
|
@ -267,19 +271,22 @@ const LibraryScreen = () => {
|
|||
<MaterialIcons
|
||||
name="video-library"
|
||||
size={80}
|
||||
color={colors.mediumGray}
|
||||
color={currentTheme.colors.mediumGray}
|
||||
style={{ opacity: 0.7 }}
|
||||
/>
|
||||
<Text style={styles.emptyText}>Your library is empty</Text>
|
||||
<Text style={styles.emptySubtext}>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>Your library is empty</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Add content to your library to keep track of what you're watching
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.exploreButton}
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => navigation.navigate('Discover')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.exploreButtonText}>Explore Content</Text>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Explore Content</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
|
|
@ -306,19 +313,16 @@ const LibraryScreen = () => {
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
headerBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: colors.darkBackground,
|
||||
zIndex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: 20,
|
||||
|
|
@ -335,7 +339,6 @@ const styles = StyleSheet.create({
|
|||
headerTitle: {
|
||||
fontSize: 32,
|
||||
fontWeight: '800',
|
||||
color: colors.white,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
filtersContainer: {
|
||||
|
|
@ -355,26 +358,17 @@ const styles = StyleSheet.create({
|
|||
marginHorizontal: 4,
|
||||
borderRadius: 24,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
shadowColor: colors.black,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
filterButtonActive: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
filterIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
filterText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.mediumGray,
|
||||
},
|
||||
filterTextActive: {
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
},
|
||||
listContainer: {
|
||||
paddingHorizontal: 12,
|
||||
|
|
@ -400,7 +394,6 @@ const styles = StyleSheet.create({
|
|||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
aspectRatio: 2/3,
|
||||
elevation: 5,
|
||||
shadowColor: colors.black,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
|
|
@ -428,7 +421,6 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
progressBar: {
|
||||
height: '100%',
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
|
|
@ -442,14 +434,12 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
},
|
||||
badgeText: {
|
||||
color: colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
itemTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
color: colors.white,
|
||||
marginBottom: 4,
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
|
|
@ -477,29 +467,24 @@ const styles = StyleSheet.create({
|
|||
emptyText: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: colors.white,
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 15,
|
||||
color: colors.mediumGray,
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
exploreButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 24,
|
||||
elevation: 3,
|
||||
shadowColor: colors.black,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
exploreButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { colors } from '../styles';
|
||||
import { catalogService, StreamingContent } from '../services/catalogService';
|
||||
import { Image } from 'expo-image';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
|
@ -42,6 +41,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { logger } from '../utils/logger';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
|
||||
|
|
@ -57,6 +57,7 @@ const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
|||
|
||||
const SkeletonLoader = () => {
|
||||
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
const pulse = RNAnimated.loop(
|
||||
|
|
@ -84,12 +85,24 @@ const SkeletonLoader = () => {
|
|||
|
||||
const renderSkeletonItem = () => (
|
||||
<View style={styles.skeletonVerticalItem}>
|
||||
<RNAnimated.View style={[styles.skeletonPoster, { opacity }]} />
|
||||
<RNAnimated.View style={[
|
||||
styles.skeletonPoster,
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]} />
|
||||
<View style={styles.skeletonItemDetails}>
|
||||
<RNAnimated.View style={[styles.skeletonTitle, { opacity }]} />
|
||||
<RNAnimated.View style={[
|
||||
styles.skeletonTitle,
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]} />
|
||||
<View style={styles.skeletonMetaRow}>
|
||||
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
|
||||
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
|
||||
<RNAnimated.View style={[
|
||||
styles.skeletonMeta,
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]} />
|
||||
<RNAnimated.View style={[
|
||||
styles.skeletonMeta,
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -100,7 +113,10 @@ const SkeletonLoader = () => {
|
|||
{[...Array(5)].map((_, index) => (
|
||||
<View key={index}>
|
||||
{index === 0 && (
|
||||
<RNAnimated.View style={[styles.skeletonSectionHeader, { opacity }]} />
|
||||
<RNAnimated.View style={[
|
||||
styles.skeletonSectionHeader,
|
||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||
]} />
|
||||
)}
|
||||
{renderSkeletonItem()}
|
||||
</View>
|
||||
|
|
@ -116,6 +132,7 @@ const SimpleSearchAnimation = () => {
|
|||
// Simple animation values that work reliably
|
||||
const spinAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||
const fadeAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Rotation animation
|
||||
|
|
@ -161,15 +178,15 @@ const SimpleSearchAnimation = () => {
|
|||
<View style={styles.simpleAnimationContent}>
|
||||
<RNAnimated.View style={[
|
||||
styles.spinnerContainer,
|
||||
{ transform: [{ rotate: spin }] }
|
||||
{ transform: [{ rotate: spin }], backgroundColor: currentTheme.colors.primary }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name="search"
|
||||
size={32}
|
||||
color={colors.white}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</RNAnimated.View>
|
||||
<Text style={styles.simpleAnimationText}>Searching</Text>
|
||||
<Text style={[styles.simpleAnimationText, { color: currentTheme.colors.white }]}>Searching</Text>
|
||||
</View>
|
||||
</RNAnimated.View>
|
||||
);
|
||||
|
|
@ -186,6 +203,7 @@ const SearchScreen = () => {
|
|||
const [showRecent, setShowRecent] = useState(true);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Animation values
|
||||
const searchBarWidth = useSharedValue(width - 32);
|
||||
|
|
@ -348,7 +366,7 @@ const SearchScreen = () => {
|
|||
style={styles.recentSearchesContainer}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<Text style={styles.carouselTitle}>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
Recent Searches
|
||||
</Text>
|
||||
{recentSearches.map((search, index) => (
|
||||
|
|
@ -364,10 +382,10 @@ const SearchScreen = () => {
|
|||
<MaterialIcons
|
||||
name="history"
|
||||
size={20}
|
||||
color={colors.lightGray}
|
||||
color={currentTheme.colors.lightGray}
|
||||
style={styles.recentSearchIcon}
|
||||
/>
|
||||
<Text style={styles.recentSearchText}>
|
||||
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
|
||||
{search}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
|
|
@ -380,7 +398,7 @@ const SearchScreen = () => {
|
|||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.recentSearchDeleteButton}
|
||||
>
|
||||
<MaterialIcons name="close" size={16} color={colors.lightGray} />
|
||||
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
</AnimatedTouchable>
|
||||
))}
|
||||
|
|
@ -398,7 +416,10 @@ const SearchScreen = () => {
|
|||
entering={FadeIn.duration(500).delay(index * 100)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.horizontalItemPosterContainer}>
|
||||
<View style={[styles.horizontalItemPosterContainer, {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
borderColor: 'rgba(255,255,255,0.05)'
|
||||
}]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
||||
style={styles.horizontalItemPoster}
|
||||
|
|
@ -406,23 +427,29 @@ const SearchScreen = () => {
|
|||
transition={300}
|
||||
/>
|
||||
<View style={styles.itemTypeContainer}>
|
||||
<Text style={styles.itemTypeText}>{item.type === 'movie' ? 'MOVIE' : 'SERIES'}</Text>
|
||||
<Text style={[styles.itemTypeText, { color: currentTheme.colors.white }]}>
|
||||
{item.type === 'movie' ? 'MOVIE' : 'SERIES'}
|
||||
</Text>
|
||||
</View>
|
||||
{item.imdbRating && (
|
||||
<View style={styles.ratingContainer}>
|
||||
<MaterialIcons name="star" size={12} color="#FFC107" />
|
||||
<Text style={styles.ratingText}>{item.imdbRating}</Text>
|
||||
<Text style={[styles.ratingText, { color: currentTheme.colors.white }]}>
|
||||
{item.imdbRating}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
style={styles.horizontalItemTitle}
|
||||
style={[styles.horizontalItemTitle, { color: currentTheme.colors.white }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.year && (
|
||||
<Text style={styles.yearText}>{item.year}</Text>
|
||||
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray }]}>
|
||||
{item.year}
|
||||
</Text>
|
||||
)}
|
||||
</AnimatedTouchable>
|
||||
);
|
||||
|
|
@ -445,7 +472,7 @@ const SearchScreen = () => {
|
|||
const headerHeight = headerBaseHeight + topSpacing + 60;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
backgroundColor="transparent"
|
||||
|
|
@ -453,56 +480,68 @@ const SearchScreen = () => {
|
|||
/>
|
||||
|
||||
{/* Fixed position header background to prevent shifts */}
|
||||
<View style={[styles.headerBackground, { height: headerHeight }]} />
|
||||
<View style={[styles.headerBackground, {
|
||||
height: headerHeight,
|
||||
backgroundColor: currentTheme.colors.darkBackground
|
||||
}]} />
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Header Section with proper top spacing */}
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
<Text style={styles.headerTitle}>Search</Text>
|
||||
<View style={[
|
||||
styles.searchBar,
|
||||
{
|
||||
backgroundColor: colors.darkGray,
|
||||
borderColor: 'transparent',
|
||||
}
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name="search"
|
||||
size={24}
|
||||
color={colors.lightGray}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.searchInput,
|
||||
{ color: colors.white }
|
||||
]}
|
||||
placeholder="Search movies, shows..."
|
||||
placeholderTextColor={colors.lightGray}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
returnKeyType="search"
|
||||
keyboardAppearance="dark"
|
||||
autoFocus
|
||||
/>
|
||||
{query.length > 0 && (
|
||||
<TouchableOpacity
|
||||
onPress={handleClearSearch}
|
||||
style={styles.clearButton}
|
||||
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
||||
>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Search</Text>
|
||||
<View style={styles.searchBarContainer}>
|
||||
<View style={[
|
||||
styles.searchBarWrapper,
|
||||
{ width: '100%' }
|
||||
]}>
|
||||
<View style={[
|
||||
styles.searchBar,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.elevation2,
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
borderWidth: 1,
|
||||
}
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name="close"
|
||||
size={20}
|
||||
color={colors.lightGray}
|
||||
name="search"
|
||||
size={24}
|
||||
color={currentTheme.colors.lightGray}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TextInput
|
||||
style={[
|
||||
styles.searchInput,
|
||||
{ color: currentTheme.colors.white }
|
||||
]}
|
||||
placeholder="Search movies, shows..."
|
||||
placeholderTextColor={currentTheme.colors.lightGray}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
returnKeyType="search"
|
||||
keyboardAppearance="dark"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
/>
|
||||
{query.length > 0 && (
|
||||
<TouchableOpacity
|
||||
onPress={handleClearSearch}
|
||||
style={styles.clearButton}
|
||||
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="close"
|
||||
size={20}
|
||||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content Container */}
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
{searching ? (
|
||||
<SimpleSearchAnimation />
|
||||
) : searched && !hasResultsToShow ? (
|
||||
|
|
@ -513,12 +552,12 @@ const SearchScreen = () => {
|
|||
<MaterialIcons
|
||||
name="search-off"
|
||||
size={64}
|
||||
color={colors.lightGray}
|
||||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
<Text style={styles.emptyText}>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
No results found
|
||||
</Text>
|
||||
<Text style={styles.emptySubtext}>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
Try different keywords or check your spelling
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
|
@ -538,7 +577,9 @@ const SearchScreen = () => {
|
|||
style={styles.carouselContainer}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<Text style={styles.carouselTitle}>Movies ({movieResults.length})</Text>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
Movies ({movieResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={movieResults}
|
||||
renderItem={renderHorizontalItem}
|
||||
|
|
@ -555,7 +596,9 @@ const SearchScreen = () => {
|
|||
style={styles.carouselContainer}
|
||||
entering={FadeIn.duration(300).delay(100)}
|
||||
>
|
||||
<Text style={styles.carouselTitle}>TV Shows ({seriesResults.length})</Text>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
TV Shows ({seriesResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={seriesResults}
|
||||
renderItem={renderHorizontalItem}
|
||||
|
|
@ -578,19 +621,16 @@ const SearchScreen = () => {
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.black,
|
||||
},
|
||||
headerBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: colors.black,
|
||||
zIndex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.black,
|
||||
paddingTop: 0,
|
||||
},
|
||||
header: {
|
||||
|
|
@ -603,26 +643,26 @@ const styles = StyleSheet.create({
|
|||
headerTitle: {
|
||||
fontSize: 32,
|
||||
fontWeight: '800',
|
||||
color: colors.white,
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 12,
|
||||
},
|
||||
searchBarContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
height: 48,
|
||||
},
|
||||
searchBarWrapper: {
|
||||
flex: 1,
|
||||
height: 48,
|
||||
},
|
||||
searchBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
height: 48,
|
||||
backgroundColor: colors.darkGray,
|
||||
height: '100%',
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
|
|
@ -632,13 +672,6 @@ const styles = StyleSheet.create({
|
|||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
backButton: {
|
||||
marginRight: 10,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
|
|
@ -646,7 +679,6 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
fontSize: 16,
|
||||
height: '100%',
|
||||
color: colors.white,
|
||||
},
|
||||
clearButton: {
|
||||
padding: 4,
|
||||
|
|
@ -664,7 +696,6 @@ const styles = StyleSheet.create({
|
|||
carouselTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: colors.white,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
|
|
@ -681,10 +712,8 @@ const styles = StyleSheet.create({
|
|||
height: HORIZONTAL_POSTER_HEIGHT,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.darkBackground,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.05)',
|
||||
},
|
||||
horizontalItemPoster: {
|
||||
width: '100%',
|
||||
|
|
@ -695,11 +724,9 @@ const styles = StyleSheet.create({
|
|||
fontWeight: '600',
|
||||
lineHeight: 18,
|
||||
textAlign: 'left',
|
||||
color: colors.white,
|
||||
},
|
||||
yearText: {
|
||||
fontSize: 12,
|
||||
color: colors.mediumGray,
|
||||
marginTop: 2,
|
||||
},
|
||||
recentSearchesContainer: {
|
||||
|
|
@ -723,7 +750,6 @@ const styles = StyleSheet.create({
|
|||
recentSearchText: {
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
color: colors.white,
|
||||
},
|
||||
recentSearchDeleteButton: {
|
||||
padding: 4,
|
||||
|
|
@ -736,7 +762,6 @@ const styles = StyleSheet.create({
|
|||
loadingText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: colors.white,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
|
|
@ -749,13 +774,11 @@ const styles = StyleSheet.create({
|
|||
fontWeight: 'bold',
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
color: colors.white,
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
color: colors.lightGray,
|
||||
},
|
||||
skeletonContainer: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -772,7 +795,6 @@ const styles = StyleSheet.create({
|
|||
width: POSTER_WIDTH,
|
||||
height: POSTER_HEIGHT,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
skeletonItemDetails: {
|
||||
flex: 1,
|
||||
|
|
@ -788,19 +810,16 @@ const styles = StyleSheet.create({
|
|||
height: 20,
|
||||
width: '80%',
|
||||
marginBottom: 8,
|
||||
backgroundColor: colors.darkBackground,
|
||||
borderRadius: 4,
|
||||
},
|
||||
skeletonMeta: {
|
||||
height: 14,
|
||||
width: '30%',
|
||||
backgroundColor: colors.darkBackground,
|
||||
borderRadius: 4,
|
||||
},
|
||||
skeletonSectionHeader: {
|
||||
height: 24,
|
||||
width: '40%',
|
||||
backgroundColor: colors.darkBackground,
|
||||
marginBottom: 16,
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
|
@ -814,7 +833,6 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 4,
|
||||
},
|
||||
itemTypeText: {
|
||||
color: colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
|
@ -830,7 +848,6 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 4,
|
||||
},
|
||||
ratingText: {
|
||||
color: colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
marginLeft: 2,
|
||||
|
|
@ -847,7 +864,6 @@ const styles = StyleSheet.create({
|
|||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: colors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
|
|
@ -861,7 +877,6 @@ const styles = StyleSheet.create({
|
|||
elevation: 3,
|
||||
},
|
||||
simpleAnimationText: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ import { useNavigation } from '@react-navigation/native';
|
|||
import { NavigationProp } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { colors } from '../styles/colors';
|
||||
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { stremioService } from '../services/stremioService';
|
||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { catalogService, DataSource } from '../services/catalogService';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
|
|
@ -39,24 +39,28 @@ interface SettingsCardProps {
|
|||
title?: string;
|
||||
}
|
||||
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => (
|
||||
<View style={[styles.cardContainer]}>
|
||||
{title && (
|
||||
<Text style={[
|
||||
styles.cardTitle,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={[styles.cardContainer]}>
|
||||
{title && (
|
||||
<Text style={[
|
||||
styles.cardTitle,
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
{title.toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
|
||||
]}>
|
||||
{title.toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
]}>
|
||||
{children}
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
interface SettingItemProps {
|
||||
title: string;
|
||||
|
|
@ -79,6 +83,8 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
isDarkMode,
|
||||
badge
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -93,21 +99,21 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
styles.settingIconContainer,
|
||||
{ backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }
|
||||
]}>
|
||||
<MaterialIcons name={icon} size={20} color={colors.primary} />
|
||||
<MaterialIcons name={icon} size={20} color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
<Text style={[styles.settingTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text style={[styles.settingDescription, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
||||
<Text style={[styles.settingDescription, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{badge && (
|
||||
<View style={[styles.badge, { backgroundColor: colors.primary }]}>
|
||||
<View style={[styles.badge, { backgroundColor: currentTheme.colors.primary }]}>
|
||||
<Text style={styles.badgeText}>{badge}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -126,6 +132,7 @@ const SettingsScreen: React.FC = () => {
|
|||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { lastUpdate } = useCatalogContext();
|
||||
const { isAuthenticated, userProfile } = useTraktContext();
|
||||
const { currentTheme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// States for dynamic content
|
||||
|
|
@ -229,8 +236,8 @@ const SettingsScreen: React.FC = () => {
|
|||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ false: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (value ? colors.white : colors.white) : ''}
|
||||
trackColor={{ false: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (value ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
|
||||
/>
|
||||
);
|
||||
|
|
@ -257,22 +264,22 @@ const SettingsScreen: React.FC = () => {
|
|||
return (
|
||||
<View style={[
|
||||
styles.container,
|
||||
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
|
||||
]}>
|
||||
{/* Fixed position header background to prevent shifts */}
|
||||
<View style={[
|
||||
styles.headerBackground,
|
||||
{ height: headerHeight, backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
||||
{ height: headerHeight, backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
|
||||
]} />
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Header Section with proper top spacing */}
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||
<Text style={[styles.headerTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
||||
Settings
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
|
||||
<Text style={[styles.resetButtonText, {color: colors.primary}]}>Reset</Text>
|
||||
<Text style={[styles.resetButtonText, {color: currentTheme.colors.primary}]}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
|
@ -399,25 +406,37 @@ const SettingsScreen: React.FC = () => {
|
|||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
|
||||
discoverDataSource === DataSource.STREMIO_ADDONS && {
|
||||
backgroundColor: currentTheme.colors.primary
|
||||
}
|
||||
]}
|
||||
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.selectorText,
|
||||
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
discoverDataSource === DataSource.STREMIO_ADDONS && {
|
||||
color: currentTheme.colors.white,
|
||||
fontWeight: '600'
|
||||
}
|
||||
]}>Addons</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
|
||||
discoverDataSource === DataSource.TMDB && {
|
||||
backgroundColor: currentTheme.colors.primary
|
||||
}
|
||||
]}
|
||||
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.selectorText,
|
||||
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
discoverDataSource === DataSource.TMDB && {
|
||||
color: currentTheme.colors.white,
|
||||
fontWeight: '600'
|
||||
}
|
||||
]}>TMDB</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -425,8 +444,32 @@ const SettingsScreen: React.FC = () => {
|
|||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard isDarkMode={isDarkMode} title="Appearance">
|
||||
<SettingItem
|
||||
title="Dark Mode"
|
||||
description="Enable dark mode for the app"
|
||||
icon="brightness-6"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings.enableDarkMode}
|
||||
onValueChange={(value) => updateSetting('enableDarkMode', value)}
|
||||
/>
|
||||
)}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Themes"
|
||||
description="Customize app colors and themes"
|
||||
icon="palette"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ThemeSettings')}
|
||||
isDarkMode={isDarkMode}
|
||||
isLast
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
<View style={styles.versionContainer}>
|
||||
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
|
||||
<Text style={[styles.versionText, {color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark}]}>
|
||||
Version 1.0.0
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -597,17 +640,9 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 12,
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
},
|
||||
selectorButtonActive: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
selectorText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: colors.mediumEmphasis,
|
||||
},
|
||||
selectorTextActive: {
|
||||
color: colors.white,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -15,12 +15,17 @@ import {
|
|||
Keyboard,
|
||||
Clipboard,
|
||||
Switch,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
TouchableWithoutFeedback,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { colors } from '../styles/colors';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
||||
|
|
@ -35,6 +40,7 @@ const TMDBSettingsScreen = () => {
|
|||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const apiKeyInputRef = useRef<TextInput>(null);
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
logger.log('[TMDBSettingsScreen] Component mounted');
|
||||
|
|
@ -217,12 +223,231 @@ const TMDBSettingsScreen = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
color: currentTheme.colors.white,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
backButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backText: {
|
||||
color: currentTheme.colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
titleContainer: {
|
||||
paddingTop: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: currentTheme.colors.white,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
switchCard: {
|
||||
backgroundColor: currentTheme.colors.elevation2,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
switchTextContainer: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
switchTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: currentTheme.colors.white,
|
||||
},
|
||||
switchDescription: {
|
||||
fontSize: 14,
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
lineHeight: 20,
|
||||
},
|
||||
statusCard: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: currentTheme.colors.elevation2,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
},
|
||||
statusIconContainer: {
|
||||
marginRight: 12,
|
||||
},
|
||||
statusTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
statusTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: currentTheme.colors.white,
|
||||
marginBottom: 4,
|
||||
},
|
||||
statusDescription: {
|
||||
fontSize: 14,
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: currentTheme.colors.elevation2,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: currentTheme.colors.white,
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
color: currentTheme.colors.white,
|
||||
fontSize: 15,
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
inputFocused: {
|
||||
borderColor: currentTheme.colors.primary,
|
||||
},
|
||||
pasteButton: {
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
padding: 4,
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
clearButton: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: currentTheme.colors.error,
|
||||
marginRight: 0,
|
||||
marginLeft: 8,
|
||||
flex: 0,
|
||||
},
|
||||
buttonText: {
|
||||
color: currentTheme.colors.white,
|
||||
fontWeight: '500',
|
||||
fontSize: 15,
|
||||
},
|
||||
clearButtonText: {
|
||||
color: currentTheme.colors.error,
|
||||
},
|
||||
resultMessage: {
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginTop: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
successMessage: {
|
||||
backgroundColor: currentTheme.colors.success + '1A', // 10% opacity
|
||||
},
|
||||
errorMessage: {
|
||||
backgroundColor: currentTheme.colors.error + '1A', // 10% opacity
|
||||
},
|
||||
resultIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
resultText: {
|
||||
flex: 1,
|
||||
},
|
||||
successText: {
|
||||
color: currentTheme.colors.success,
|
||||
},
|
||||
errorText: {
|
||||
color: currentTheme.colors.error,
|
||||
},
|
||||
helpLink: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
helpIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
helpText: {
|
||||
color: currentTheme.colors.primary,
|
||||
fontSize: 14,
|
||||
},
|
||||
infoCard: {
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
infoIcon: {
|
||||
marginRight: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
infoText: {
|
||||
color: currentTheme.colors.mediumEmphasis,
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={styles.loadingText}>Loading Settings...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
|
@ -237,42 +462,43 @@ const TMDBSettingsScreen = () => {
|
|||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>TMDb Settings</Text>
|
||||
<Text style={styles.title}>TMDb Settings</Text>
|
||||
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.switchCard}>
|
||||
<View style={styles.switchRow}>
|
||||
<Text style={styles.switchLabel}>Use Custom TMDb API Key</Text>
|
||||
<Switch
|
||||
value={useCustomKey}
|
||||
onValueChange={toggleUseCustomKey}
|
||||
trackColor={{ false: colors.lightGray, true: colors.accentLight }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.primary : ''}
|
||||
ios_backgroundColor={colors.lightGray}
|
||||
/>
|
||||
<View style={styles.switchTextContainer}>
|
||||
<Text style={styles.switchTitle}>Use Custom TMDb API Key</Text>
|
||||
</View>
|
||||
<Text style={styles.switchDescription}>
|
||||
Enable to use your own TMDb API key instead of the built-in one.
|
||||
Using your own API key may provide better performance and higher rate limits.
|
||||
</Text>
|
||||
<Switch
|
||||
value={useCustomKey}
|
||||
onValueChange={toggleUseCustomKey}
|
||||
trackColor={{ false: currentTheme.colors.lightGray, true: currentTheme.colors.accentLight }}
|
||||
thumbColor={Platform.OS === 'android' ? currentTheme.colors.primary : ''}
|
||||
ios_backgroundColor={currentTheme.colors.lightGray}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.switchDescription}>
|
||||
Enable to use your own TMDb API key instead of the built-in one.
|
||||
Using your own API key may provide better performance and higher rate limits.
|
||||
</Text>
|
||||
|
||||
{useCustomKey && (
|
||||
<>
|
||||
<View style={styles.statusCard}>
|
||||
<MaterialIcons
|
||||
name={isKeySet ? "check-circle" : "error-outline"}
|
||||
size={28}
|
||||
color={isKeySet ? colors.success : colors.warning}
|
||||
style={styles.statusIcon}
|
||||
color={isKeySet ? currentTheme.colors.success : currentTheme.colors.warning}
|
||||
style={styles.statusIconContainer}
|
||||
/>
|
||||
<View style={styles.statusTextContainer}>
|
||||
<Text style={styles.statusTitle}>
|
||||
|
|
@ -287,8 +513,8 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.sectionTitle}>API Key</Text>
|
||||
<View style={styles.inputWrapper}>
|
||||
<Text style={styles.cardTitle}>API Key</Text>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
ref={apiKeyInputRef}
|
||||
style={[styles.input, isInputFocused && styles.inputFocused]}
|
||||
|
|
@ -298,7 +524,7 @@ const TMDBSettingsScreen = () => {
|
|||
if (testResult) setTestResult(null);
|
||||
}}
|
||||
placeholder="Paste your TMDb API key (v4 auth)"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
placeholderTextColor={currentTheme.colors.mediumGray}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
spellCheck={false}
|
||||
|
|
@ -309,7 +535,7 @@ const TMDBSettingsScreen = () => {
|
|||
style={styles.pasteButton}
|
||||
onPress={pasteFromClipboard}
|
||||
>
|
||||
<MaterialIcons name="content-paste" size={20} color={colors.primary} />
|
||||
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
|
@ -339,7 +565,7 @@ const TMDBSettingsScreen = () => {
|
|||
<MaterialIcons
|
||||
name={testResult.success ? "check-circle" : "error"}
|
||||
size={18}
|
||||
color={testResult.success ? colors.success : colors.error}
|
||||
color={testResult.success ? currentTheme.colors.success : currentTheme.colors.error}
|
||||
style={styles.resultIcon}
|
||||
/>
|
||||
<Text style={[
|
||||
|
|
@ -355,7 +581,7 @@ const TMDBSettingsScreen = () => {
|
|||
style={styles.helpLink}
|
||||
onPress={openTMDBWebsite}
|
||||
>
|
||||
<MaterialIcons name="help" size={16} color={colors.primary} style={styles.helpIcon} />
|
||||
<MaterialIcons name="help" size={16} color={currentTheme.colors.primary} style={styles.helpIcon} />
|
||||
<Text style={styles.helpText}>
|
||||
How to get a TMDb API key?
|
||||
</Text>
|
||||
|
|
@ -363,7 +589,7 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
|
||||
<View style={styles.infoCard}>
|
||||
<MaterialIcons name="info-outline" size={22} color={colors.primary} style={styles.infoIcon} />
|
||||
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
|
||||
<Text style={styles.infoText}>
|
||||
To get your own TMDb API key (v4 auth token), you need to create a TMDb account and request an API key from their website.
|
||||
Using your own API key gives you dedicated quota and may improve app performance.
|
||||
|
|
@ -374,7 +600,7 @@ const TMDBSettingsScreen = () => {
|
|||
|
||||
{!useCustomKey && (
|
||||
<View style={styles.infoCard}>
|
||||
<MaterialIcons name="info-outline" size={22} color={colors.primary} style={styles.infoIcon} />
|
||||
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
|
||||
<Text style={styles.infoText}>
|
||||
Currently using the built-in TMDb API key. This key is shared among all users.
|
||||
For better performance and reliability, consider using your own API key.
|
||||
|
|
@ -386,236 +612,4 @@ const TMDBSettingsScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
color: colors.white,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
backButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backText: {
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: colors.white,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
},
|
||||
switchCard: {
|
||||
backgroundColor: colors.elevation2,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
switchRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
switchLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
},
|
||||
switchDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumEmphasis,
|
||||
lineHeight: 20,
|
||||
},
|
||||
statusCard: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.elevation2,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
statusIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
statusTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
statusTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
marginBottom: 4,
|
||||
},
|
||||
statusDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumEmphasis,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.elevation2,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.elevation1,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
color: colors.white,
|
||||
fontSize: 15,
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
inputFocused: {
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
pasteButton: {
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
padding: 8,
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 16,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
clearButton: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error,
|
||||
marginRight: 0,
|
||||
marginLeft: 8,
|
||||
flex: 0,
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.white,
|
||||
fontWeight: '500',
|
||||
fontSize: 15,
|
||||
},
|
||||
clearButtonText: {
|
||||
color: colors.error,
|
||||
},
|
||||
resultMessage: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
successMessage: {
|
||||
backgroundColor: colors.success + '1A', // 10% opacity
|
||||
},
|
||||
errorMessage: {
|
||||
backgroundColor: colors.error + '1A', // 10% opacity
|
||||
},
|
||||
resultIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
resultText: {
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
},
|
||||
successText: {
|
||||
color: colors.success,
|
||||
},
|
||||
errorText: {
|
||||
color: colors.error,
|
||||
},
|
||||
helpLink: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
},
|
||||
helpIcon: {
|
||||
marginRight: 6,
|
||||
},
|
||||
helpText: {
|
||||
color: colors.primary,
|
||||
fontSize: 14,
|
||||
},
|
||||
infoCard: {
|
||||
backgroundColor: colors.elevation1,
|
||||
borderRadius: 12,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
infoIcon: {
|
||||
marginRight: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
infoText: {
|
||||
color: colors.mediumEmphasis,
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default TMDBSettingsScreen;
|
||||
569
src/screens/ThemeScreen.tsx
Normal file
569
src/screens/ThemeScreen.tsx
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Switch,
|
||||
ScrollView,
|
||||
Alert,
|
||||
Platform,
|
||||
TextInput,
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import ColorPicker from 'react-native-wheel-color-picker';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { colors } from '../styles/colors';
|
||||
import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
interface ThemeCardProps {
|
||||
theme: Theme;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const ThemeCard: React.FC<ThemeCardProps> = ({
|
||||
theme,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onDelete
|
||||
}) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.themeCard,
|
||||
isSelected && styles.selectedThemeCard,
|
||||
{ borderColor: theme.colors.primary }
|
||||
]}
|
||||
onPress={onSelect}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.themeCardHeader}>
|
||||
<Text style={[styles.themeCardTitle, { color: theme.colors.text }]}>
|
||||
{theme.name}
|
||||
</Text>
|
||||
{isSelected && (
|
||||
<MaterialIcons name="check-circle" size={20} color={theme.colors.primary} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.colorPreviewContainer}>
|
||||
<View style={[styles.colorPreview, { backgroundColor: theme.colors.primary }]}>
|
||||
<Text style={styles.colorPreviewLabel}>Primary</Text>
|
||||
</View>
|
||||
<View style={[styles.colorPreview, { backgroundColor: theme.colors.secondary }]}>
|
||||
<Text style={styles.colorPreviewLabel}>Secondary</Text>
|
||||
</View>
|
||||
<View style={[styles.colorPreview, { backgroundColor: theme.colors.darkBackground }]}>
|
||||
<Text style={styles.colorPreviewLabel}>Background</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{theme.isEditable && (
|
||||
<View style={styles.themeCardActions}>
|
||||
{onEdit && (
|
||||
<TouchableOpacity style={styles.themeCardAction} onPress={onEdit}>
|
||||
<MaterialIcons name="edit" size={18} color={theme.colors.primary} />
|
||||
<Text style={[styles.themeCardActionText, { color: theme.colors.primary }]}>Edit</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity style={styles.themeCardAction} onPress={onDelete}>
|
||||
<MaterialIcons name="delete" size={18} color={theme.colors.error} />
|
||||
<Text style={[styles.themeCardActionText, { color: theme.colors.error }]}>Delete</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
type ColorKey = 'primary' | 'secondary' | 'darkBackground';
|
||||
|
||||
interface ThemeColorEditorProps {
|
||||
initialColors: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
darkBackground: string;
|
||||
};
|
||||
onSave: (colors: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
darkBackground: string;
|
||||
name: string;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const ThemeColorEditor: React.FC<ThemeColorEditorProps> = ({
|
||||
initialColors,
|
||||
onSave,
|
||||
onCancel
|
||||
}) => {
|
||||
const [themeName, setThemeName] = useState('Custom Theme');
|
||||
const [selectedColorKey, setSelectedColorKey] = useState<ColorKey>('primary');
|
||||
const [themeColors, setThemeColors] = useState({
|
||||
primary: initialColors.primary,
|
||||
secondary: initialColors.secondary,
|
||||
darkBackground: initialColors.darkBackground,
|
||||
});
|
||||
|
||||
const handleColorChange = useCallback((color: string) => {
|
||||
setThemeColors(prev => ({
|
||||
...prev,
|
||||
[selectedColorKey]: color,
|
||||
}));
|
||||
}, [selectedColorKey]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!themeName.trim()) {
|
||||
Alert.alert('Invalid Name', 'Please enter a valid theme name');
|
||||
return;
|
||||
}
|
||||
onSave({
|
||||
...themeColors,
|
||||
name: themeName
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.editorContainer}>
|
||||
<Text style={styles.editorTitle}>Custom Theme</Text>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.inputLabel}>Theme Name</Text>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
value={themeName}
|
||||
onChangeText={setThemeName}
|
||||
placeholder="Enter theme name"
|
||||
placeholderTextColor="rgba(255,255,255,0.5)"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.colorSelectorContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.colorSelectorButton,
|
||||
selectedColorKey === 'primary' && styles.selectedColorButton,
|
||||
{ backgroundColor: themeColors.primary }
|
||||
]}
|
||||
onPress={() => setSelectedColorKey('primary')}
|
||||
>
|
||||
<Text style={styles.colorButtonText}>Primary</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.colorSelectorButton,
|
||||
selectedColorKey === 'secondary' && styles.selectedColorButton,
|
||||
{ backgroundColor: themeColors.secondary }
|
||||
]}
|
||||
onPress={() => setSelectedColorKey('secondary')}
|
||||
>
|
||||
<Text style={styles.colorButtonText}>Secondary</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.colorSelectorButton,
|
||||
selectedColorKey === 'darkBackground' && styles.selectedColorButton,
|
||||
{ backgroundColor: themeColors.darkBackground }
|
||||
]}
|
||||
onPress={() => setSelectedColorKey('darkBackground')}
|
||||
>
|
||||
<Text style={styles.colorButtonText}>Background</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.colorPickerContainer}>
|
||||
<ColorPicker
|
||||
color={themeColors[selectedColorKey]}
|
||||
onColorChange={handleColorChange}
|
||||
thumbSize={30}
|
||||
sliderSize={30}
|
||||
noSnap={true}
|
||||
row={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.editorActions}>
|
||||
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.saveButton} onPress={handleSave}>
|
||||
<Text style={styles.saveButtonText}>Save Theme</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ThemeScreen: React.FC = () => {
|
||||
const {
|
||||
currentTheme,
|
||||
availableThemes,
|
||||
setCurrentTheme,
|
||||
addCustomTheme,
|
||||
updateCustomTheme,
|
||||
deleteCustomTheme
|
||||
} = useTheme();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [editingTheme, setEditingTheme] = useState<Theme | null>(null);
|
||||
|
||||
// Force consistent status bar settings
|
||||
useEffect(() => {
|
||||
const applyStatusBarConfig = () => {
|
||||
if (Platform.OS === 'android') {
|
||||
StatusBar.setTranslucent(true);
|
||||
StatusBar.setBackgroundColor('transparent');
|
||||
}
|
||||
};
|
||||
|
||||
applyStatusBarConfig();
|
||||
|
||||
// Re-apply on focus
|
||||
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
const handleThemeSelect = useCallback((themeId: string) => {
|
||||
setCurrentTheme(themeId);
|
||||
}, [setCurrentTheme]);
|
||||
|
||||
const handleEditTheme = useCallback((theme: Theme) => {
|
||||
setEditingTheme(theme);
|
||||
setIsEditMode(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteTheme = useCallback((theme: Theme) => {
|
||||
Alert.alert(
|
||||
'Delete Theme',
|
||||
`Are you sure you want to delete "${theme.name}"?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: () => deleteCustomTheme(theme.id)
|
||||
}
|
||||
]
|
||||
);
|
||||
}, [deleteCustomTheme]);
|
||||
|
||||
const handleCreateTheme = useCallback(() => {
|
||||
setEditingTheme(null);
|
||||
setIsEditMode(true);
|
||||
}, []);
|
||||
|
||||
const handleSaveTheme = useCallback((themeData: any) => {
|
||||
if (editingTheme) {
|
||||
// Update existing theme
|
||||
updateCustomTheme({
|
||||
...editingTheme,
|
||||
name: themeData.name || editingTheme.name,
|
||||
colors: {
|
||||
...editingTheme.colors,
|
||||
primary: themeData.primary,
|
||||
secondary: themeData.secondary,
|
||||
darkBackground: themeData.darkBackground,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Create new theme
|
||||
addCustomTheme({
|
||||
name: themeData.name || 'Custom Theme',
|
||||
colors: {
|
||||
...currentTheme.colors,
|
||||
primary: themeData.primary,
|
||||
secondary: themeData.secondary,
|
||||
darkBackground: themeData.darkBackground,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setIsEditMode(false);
|
||||
setEditingTheme(null);
|
||||
}, [editingTheme, updateCustomTheme, addCustomTheme, currentTheme]);
|
||||
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setIsEditMode(false);
|
||||
setEditingTheme(null);
|
||||
}, []);
|
||||
|
||||
if (isEditMode) {
|
||||
const initialColors = editingTheme ? {
|
||||
primary: editingTheme.colors.primary,
|
||||
secondary: editingTheme.colors.secondary,
|
||||
darkBackground: editingTheme.colors.darkBackground,
|
||||
} : {
|
||||
primary: currentTheme.colors.primary,
|
||||
secondary: currentTheme.colors.secondary,
|
||||
darkBackground: currentTheme.colors.darkBackground,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: insets.bottom,
|
||||
}
|
||||
]}>
|
||||
<ThemeColorEditor
|
||||
initialColors={initialColors}
|
||||
onSave={handleSaveTheme}
|
||||
onCancel={handleCancelEdit}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: insets.bottom,
|
||||
}
|
||||
]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>App Themes</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted }]}>
|
||||
SELECT THEME
|
||||
</Text>
|
||||
|
||||
<View style={styles.themeGrid}>
|
||||
{availableThemes.map(theme => (
|
||||
<ThemeCard
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
isSelected={currentTheme.id === theme.id}
|
||||
onSelect={() => handleThemeSelect(theme.id)}
|
||||
onEdit={theme.isEditable ? () => handleEditTheme(theme) : undefined}
|
||||
onDelete={theme.isEditable ? () => handleDeleteTheme(theme) : undefined}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.createButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={handleCreateTheme}
|
||||
>
|
||||
<MaterialIcons name="add" size={24} color="#FFFFFF" />
|
||||
<Text style={styles.createButtonText}>Create Custom Theme</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 16,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
},
|
||||
themeGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
themeCard: {
|
||||
width: (width - 48) / 2,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
selectedThemeCard: {
|
||||
borderWidth: 2,
|
||||
},
|
||||
themeCardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
themeCardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
colorPreviewContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
},
|
||||
colorPreview: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
colorPreviewLabel: {
|
||||
fontSize: 6,
|
||||
color: '#FFFFFF',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
},
|
||||
themeCardActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
themeCardAction: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 4,
|
||||
},
|
||||
themeCardActionText: {
|
||||
fontSize: 12,
|
||||
marginLeft: 4,
|
||||
},
|
||||
createButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
createButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 8,
|
||||
},
|
||||
editorContainer: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
editorTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
marginBottom: 8,
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
},
|
||||
colorSelectorContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 24,
|
||||
},
|
||||
colorSelectorButton: {
|
||||
width: (width - 64) / 3,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectedColorButton: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
colorButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
},
|
||||
colorPickerContainer: {
|
||||
height: 300,
|
||||
marginBottom: 24,
|
||||
},
|
||||
editorActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
cancelButton: {
|
||||
width: (width - 48) / 2,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
saveButton: {
|
||||
width: (width - 48) / 2,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
saveButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
export default ThemeScreen;
|
||||
|
|
@ -16,10 +16,10 @@ import { useNavigation } from '@react-navigation/native';
|
|||
import { makeRedirectUri, useAuthRequest, ResponseType, Prompt, CodeChallengeMethod } from 'expo-auth-session';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { traktService, TraktUser } from '../services/traktService';
|
||||
import { colors } from '../styles/colors';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { logger } from '../utils/logger';
|
||||
import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
|
|
@ -43,6 +43,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
|
@ -151,7 +152,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
return (
|
||||
<SafeAreaView style={[
|
||||
styles.container,
|
||||
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
|
||||
]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<View style={styles.header}>
|
||||
|
|
@ -162,12 +163,12 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
<MaterialIcons
|
||||
name="arrow-back"
|
||||
size={24}
|
||||
color={isDarkMode ? colors.highEmphasis : colors.textDark}
|
||||
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={[
|
||||
styles.headerTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||
]}>
|
||||
Trakt Settings
|
||||
</Text>
|
||||
|
|
@ -179,11 +180,11 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
>
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
|
||||
]}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
) : isAuthenticated && userProfile ? (
|
||||
<View style={styles.profileContainer}>
|
||||
|
|
@ -194,7 +195,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
style={styles.avatar}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.avatarPlaceholder, { backgroundColor: colors.primary }]}>
|
||||
<View style={[styles.avatarPlaceholder, { backgroundColor: currentTheme.colors.primary }]}>
|
||||
<Text style={styles.avatarText}>
|
||||
{userProfile.name?.charAt(0) || userProfile.username.charAt(0)}
|
||||
</Text>
|
||||
|
|
@ -203,13 +204,13 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
<View style={styles.profileInfo}>
|
||||
<Text style={[
|
||||
styles.profileName,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||
]}>
|
||||
{userProfile.name || userProfile.username}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.profileUsername,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
@{userProfile.username}
|
||||
</Text>
|
||||
|
|
@ -224,7 +225,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
<View style={styles.statsContainer}>
|
||||
<Text style={[
|
||||
styles.joinedDate,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
Joined {new Date(userProfile.joined_at).toLocaleDateString()}
|
||||
</Text>
|
||||
|
|
@ -252,20 +253,20 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
/>
|
||||
<Text style={[
|
||||
styles.signInTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||
]}>
|
||||
Connect with Trakt
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.signInDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
Sync your watch history, watchlist, and collection with Trakt.tv
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{ backgroundColor: isDarkMode ? colors.primary : colors.primary }
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={handleSignIn}
|
||||
disabled={!request || isExchangingCode} // Disable while waiting for response or exchanging code
|
||||
|
|
@ -285,25 +286,25 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
{isAuthenticated && (
|
||||
<View style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
|
||||
]}>
|
||||
<View style={styles.settingsSection}>
|
||||
<Text style={[
|
||||
styles.sectionTitle,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||
]}>
|
||||
Sync Settings
|
||||
</Text>
|
||||
<View style={styles.settingItem}>
|
||||
<Text style={[
|
||||
styles.settingLabel,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||
]}>
|
||||
Auto-sync playback progress
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.settingDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
Coming soon
|
||||
</Text>
|
||||
|
|
@ -311,13 +312,13 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
<View style={styles.settingItem}>
|
||||
<Text style={[
|
||||
styles.settingLabel,
|
||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||
]}>
|
||||
Import watched history
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.settingDescription,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
Coming soon
|
||||
</Text>
|
||||
|
|
@ -331,7 +332,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
>
|
||||
<Text style={[
|
||||
styles.buttonText,
|
||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
||||
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||
]}>
|
||||
Sync Now (Coming Soon)
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -1,55 +1,40 @@
|
|||
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||
import { colors } from './colors';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
export const POSTER_WIDTH = (width - 50) / 3;
|
||||
export const POSTER_HEIGHT = POSTER_WIDTH * 1.5;
|
||||
export const HORIZONTAL_PADDING = 16;
|
||||
|
||||
export const homeStyles = StyleSheet.create({
|
||||
export const sharedStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
loadingMainContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 40,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: HORIZONTAL_PADDING,
|
||||
},
|
||||
loadingText: {
|
||||
color: colors.textMuted,
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
emptyCatalog: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.elevation1,
|
||||
margin: 16,
|
||||
borderRadius: 16,
|
||||
},
|
||||
addCatalogButton: {
|
||||
seeAllButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 30,
|
||||
marginTop: 16,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
addCatalogButtonText: {
|
||||
color: colors.white,
|
||||
seeAllText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default homeStyles;
|
||||
export default {
|
||||
POSTER_WIDTH,
|
||||
POSTER_HEIGHT,
|
||||
HORIZONTAL_PADDING,
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue