mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-10 12:01:55 +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 { CatalogProvider } from './src/contexts/CatalogContext';
|
||||||
import { GenreProvider } from './src/contexts/GenreContext';
|
import { GenreProvider } from './src/contexts/GenreContext';
|
||||||
import { TraktProvider } from './src/contexts/TraktContext';
|
import { TraktProvider } from './src/contexts/TraktContext';
|
||||||
|
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
|
||||||
|
|
||||||
// This fixes many navigation layout issues by using native screen containers
|
// This fixes many navigation layout issues by using native screen containers
|
||||||
enableScreens(true);
|
enableScreens(true);
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
// Inner app component that uses the theme context
|
||||||
// Always use dark mode
|
const ThemedApp = () => {
|
||||||
const isDarkMode = true;
|
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 (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<GenreProvider>
|
<GenreProvider>
|
||||||
<CatalogProvider>
|
<CatalogProvider>
|
||||||
<TraktProvider>
|
<TraktProvider>
|
||||||
<PaperProvider theme={CustomDarkTheme}>
|
<ThemeProvider>
|
||||||
<NavigationContainer
|
<ThemedApp />
|
||||||
theme={CustomNavigationDarkTheme}
|
</ThemeProvider>
|
||||||
// Disable automatic linking which can cause layout issues
|
|
||||||
linking={undefined}
|
|
||||||
>
|
|
||||||
<View style={[styles.container, { backgroundColor: '#000000' }]}>
|
|
||||||
<StatusBar
|
|
||||||
style="light"
|
|
||||||
/>
|
|
||||||
<AppNavigator />
|
|
||||||
</View>
|
|
||||||
</NavigationContainer>
|
|
||||||
</PaperProvider>
|
|
||||||
</TraktProvider>
|
</TraktProvider>
|
||||||
</CatalogProvider>
|
</CatalogProvider>
|
||||||
</GenreProvider>
|
</GenreProvider>
|
||||||
|
|
|
||||||
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -61,6 +61,7 @@
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
|
"react-native-wheel-color-picker": "^1.3.1",
|
||||||
"subsrt": "^1.1.1"
|
"subsrt": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -10840,6 +10841,12 @@
|
||||||
"react-native-reanimated": ">=2.8.0"
|
"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": {
|
"node_modules/react-native-gesture-handler": {
|
||||||
"version": "2.20.2",
|
"version": "2.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
|
"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==",
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
|
||||||
"version": "0.23.1",
|
"version": "0.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz",
|
"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-url-polyfill": "^2.0.0",
|
||||||
"react-native-video": "^6.12.0",
|
"react-native-video": "^6.12.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
|
"react-native-wheel-color-picker": "^1.3.1",
|
||||||
"subsrt": "^1.1.1"
|
"subsrt": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, TouchableOpacity, Platform, StyleSheet, Image } from 'react-native';
|
import { View, TouchableOpacity, Platform, StyleSheet, Image } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../styles/colors';
|
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import type { RootStackParamList } from '../navigation/AppNavigator';
|
import type { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||||
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
|
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
|
||||||
import Constants, { ExecutionEnvironment } from 'expo-constants';
|
import Constants, { ExecutionEnvironment } from 'expo-constants';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
export const NuvioHeader = () => {
|
export const NuvioHeader = () => {
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
// Only render the header if the current route is 'Home'
|
// Only render the header if the current route is 'Home'
|
||||||
if (route.name !== 'Home') {
|
if (route.name !== 'Home') {
|
||||||
|
|
@ -59,7 +60,7 @@ export const NuvioHeader = () => {
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="magnify"
|
name="magnify"
|
||||||
size={24}
|
size={24}
|
||||||
color={colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { View, Text, TouchableOpacity, StyleSheet, FlatList, Dimensions } from '
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } 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 { GenreCatalog, Category } from '../../constants/discover';
|
||||||
import { StreamingContent } from '../../services/catalogService';
|
import { StreamingContent } from '../../services/catalogService';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
|
@ -16,6 +16,7 @@ interface CatalogSectionProps {
|
||||||
|
|
||||||
const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
|
const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
|
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.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.title}>{catalog.genre}</Text>
|
<Text style={[styles.title, { color: currentTheme.colors.white }]}>
|
||||||
<View style={styles.titleBar} />
|
{catalog.genre}
|
||||||
|
</Text>
|
||||||
|
<View style={[styles.titleBar, { backgroundColor: currentTheme.colors.primary }]} />
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleSeeMorePress}
|
onPress={handleSeeMorePress}
|
||||||
style={styles.seeAllButton}
|
style={styles.seeAllButton}
|
||||||
activeOpacity={0.6}
|
activeOpacity={0.6}
|
||||||
>
|
>
|
||||||
<Text style={styles.seeAllText}>See All</Text>
|
<Text style={[styles.seeAllText, { color: currentTheme.colors.primary }]}>See All</Text>
|
||||||
<MaterialIcons name="arrow-forward-ios" color={colors.primary} size={14} />
|
<MaterialIcons name="arrow-forward-ios" color={currentTheme.colors.primary} size={14} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -106,14 +109,12 @@ const styles = StyleSheet.create({
|
||||||
titleBar: {
|
titleBar: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 3,
|
height: 3,
|
||||||
backgroundColor: colors.primary,
|
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
seeAllButton: {
|
seeAllButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -123,7 +124,6 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
seeAllText: {
|
seeAllText: {
|
||||||
color: colors.primary,
|
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../styles';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { Category } from '../../constants/discover';
|
import { Category } from '../../constants/discover';
|
||||||
|
|
||||||
interface CategorySelectorProps {
|
interface CategorySelectorProps {
|
||||||
|
|
@ -15,6 +15,7 @@ const CategorySelector = ({
|
||||||
selectedCategory,
|
selectedCategory,
|
||||||
onSelectCategory
|
onSelectCategory
|
||||||
}: CategorySelectorProps) => {
|
}: CategorySelectorProps) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
const renderCategoryButton = useCallback((category: Category) => {
|
const renderCategoryButton = useCallback((category: Category) => {
|
||||||
const isSelected = selectedCategory.id === category.id;
|
const isSelected = selectedCategory.id === category.id;
|
||||||
|
|
@ -24,7 +25,7 @@ const CategorySelector = ({
|
||||||
key={category.id}
|
key={category.id}
|
||||||
style={[
|
style={[
|
||||||
styles.categoryButton,
|
styles.categoryButton,
|
||||||
isSelected && styles.selectedCategoryButton
|
isSelected && { backgroundColor: currentTheme.colors.primary }
|
||||||
]}
|
]}
|
||||||
onPress={() => onSelectCategory(category)}
|
onPress={() => onSelectCategory(category)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -32,19 +33,19 @@ const CategorySelector = ({
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={category.icon}
|
name={category.icon}
|
||||||
size={24}
|
size={24}
|
||||||
color={isSelected ? colors.white : colors.mediumGray}
|
color={isSelected ? currentTheme.colors.white : currentTheme.colors.mediumGray}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.categoryText,
|
styles.categoryText,
|
||||||
isSelected && styles.selectedCategoryText
|
isSelected && { color: currentTheme.colors.white, fontWeight: '700' }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{category.name}
|
{category.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}, [selectedCategory, onSelectCategory]);
|
}, [selectedCategory, onSelectCategory, currentTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|
@ -78,24 +79,17 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
maxWidth: 160,
|
maxWidth: 160,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
shadowColor: colors.black,
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
},
|
},
|
||||||
selectedCategoryButton: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
categoryText: {
|
categoryText: {
|
||||||
color: colors.mediumGray,
|
color: '#9e9e9e', // Default medium gray
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
selectedCategoryText: {
|
|
||||||
color: colors.white,
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(CategorySelector);
|
export default React.memo(CategorySelector);
|
||||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
|
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { colors } from '../../styles';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { StreamingContent } from '../../services/catalogService';
|
import { StreamingContent } from '../../services/catalogService';
|
||||||
|
|
||||||
interface ContentItemProps {
|
interface ContentItemProps {
|
||||||
|
|
@ -13,6 +13,7 @@ interface ContentItemProps {
|
||||||
|
|
||||||
const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
const { width: screenWidth } = Dimensions.get('window');
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
const itemWidth = width || (screenWidth - 48) / 2.2; // Default to 2 items per row with spacing
|
const itemWidth = width || (screenWidth - 48) / 2.2; // Default to 2 items per row with spacing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -21,7 +22,7 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.6}
|
activeOpacity={0.6}
|
||||||
>
|
>
|
||||||
<View style={styles.posterContainer}>
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||||
style={styles.poster}
|
style={styles.poster}
|
||||||
|
|
@ -33,7 +34,7 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
||||||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||||
style={styles.gradient}
|
style={styles.gradient}
|
||||||
>
|
>
|
||||||
<Text style={styles.title} numberOfLines={2}>
|
<Text style={[styles.title, { color: currentTheme.colors.white }]} numberOfLines={2}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.year && (
|
{item.year && (
|
||||||
|
|
@ -54,7 +55,6 @@ const styles = StyleSheet.create({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
shadowColor: colors.black,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.2,
|
shadowOpacity: 0.2,
|
||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
|
|
@ -75,7 +75,6 @@ const styles = StyleSheet.create({
|
||||||
title: {
|
title: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.white,
|
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
textShadowOffset: { width: 0, height: 1 },
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
||||||
import { colors } from '../../styles';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
|
||||||
interface GenreSelectorProps {
|
interface GenreSelectorProps {
|
||||||
genres: string[];
|
genres: string[];
|
||||||
|
|
@ -13,6 +13,7 @@ const GenreSelector = ({
|
||||||
selectedGenre,
|
selectedGenre,
|
||||||
onSelectGenre
|
onSelectGenre
|
||||||
}: GenreSelectorProps) => {
|
}: GenreSelectorProps) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
const renderGenreButton = useCallback((genre: string) => {
|
const renderGenreButton = useCallback((genre: string) => {
|
||||||
const isSelected = selectedGenre === genre;
|
const isSelected = selectedGenre === genre;
|
||||||
|
|
@ -22,7 +23,7 @@ const GenreSelector = ({
|
||||||
key={genre}
|
key={genre}
|
||||||
style={[
|
style={[
|
||||||
styles.genreButton,
|
styles.genreButton,
|
||||||
isSelected && styles.selectedGenreButton
|
isSelected && { backgroundColor: currentTheme.colors.primary }
|
||||||
]}
|
]}
|
||||||
onPress={() => onSelectGenre(genre)}
|
onPress={() => onSelectGenre(genre)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -30,14 +31,14 @@ const GenreSelector = ({
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.genreText,
|
styles.genreText,
|
||||||
isSelected && styles.selectedGenreText
|
isSelected && { color: currentTheme.colors.white, fontWeight: '600' }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{genre}
|
{genre}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}, [selectedGenre, onSelectGenre]);
|
}, [selectedGenre, onSelectGenre, currentTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|
@ -70,25 +71,18 @@ const styles = StyleSheet.create({
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||||
shadowColor: colors.black,
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
selectedGenreButton: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
genreText: {
|
genreText: {
|
||||||
color: colors.mediumGray,
|
color: '#9e9e9e', // Default medium gray
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
selectedGenreText: {
|
|
||||||
color: colors.white,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(GenreSelector);
|
export default React.memo(GenreSelector);
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
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 { Image as ExpoImage } from 'expo-image';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../styles/colors';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { catalogService, StreamingContent } from '../../services/catalogService';
|
import { catalogService, StreamingContent } from '../../services/catalogService';
|
||||||
import DropUpMenu from './DropUpMenu';
|
import DropUpMenu from './DropUpMenu';
|
||||||
|
|
||||||
|
|
@ -20,6 +20,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
||||||
const [isWatched, setIsWatched] = useState(false);
|
const [isWatched, setIsWatched] = useState(false);
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
const handleLongPress = useCallback(() => {
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
|
|
@ -95,22 +96,22 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{(!imageLoaded || imageError) && (
|
{(!imageLoaded || imageError) && (
|
||||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
|
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||||
{!imageError ? (
|
{!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>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isWatched && (
|
{isWatched && (
|
||||||
<View style={styles.watchedIndicator}>
|
<View style={styles.watchedIndicator}>
|
||||||
<MaterialIcons name="check-circle" size={22} color={colors.success} />
|
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{localItem.inLibrary && (
|
{localItem.inLibrary && (
|
||||||
<View style={styles.libraryBadge}>
|
<View style={styles.libraryBadge}>
|
||||||
<MaterialIcons name="bookmark" size={16} color={colors.white} />
|
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -160,7 +161,6 @@ const styles = StyleSheet.create({
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|
@ -169,7 +169,6 @@ const styles = StyleSheet.create({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
backgroundColor: colors.transparentDark,
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
},
|
},
|
||||||
|
|
@ -177,7 +176,6 @@ const styles = StyleSheet.create({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 8,
|
left: 8,
|
||||||
backgroundColor: colors.transparentDark,
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../styles/colors';
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
|
|
@ -32,6 +31,8 @@ import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { TMDBService } from '../../services/tmdbService';
|
import { TMDBService } from '../../services/tmdbService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import type { Theme } from '../../contexts/ThemeContext';
|
||||||
|
|
||||||
interface FeaturedContentProps {
|
interface FeaturedContentProps {
|
||||||
featuredContent: StreamingContent | null;
|
featuredContent: StreamingContent | null;
|
||||||
|
|
@ -47,6 +48,7 @@ const { width, height } = Dimensions.get('window');
|
||||||
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
|
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||||
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
|
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
|
||||||
const prevContentIdRef = useRef<string | null>(null);
|
const prevContentIdRef = useRef<string | null>(null);
|
||||||
|
|
@ -350,7 +352,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
||||||
'transparent',
|
'transparent',
|
||||||
'rgba(0,0,0,0.1)',
|
'rgba(0,0,0,0.1)',
|
||||||
'rgba(0,0,0,0.7)',
|
'rgba(0,0,0,0.7)',
|
||||||
colors.darkBackground,
|
currentTheme.colors.darkBackground,
|
||||||
]}
|
]}
|
||||||
locations={[0, 0.3, 0.7, 1]}
|
locations={[0, 0.3, 0.7, 1]}
|
||||||
style={styles.featuredGradient as ViewStyle}
|
style={styles.featuredGradient as ViewStyle}
|
||||||
|
|
@ -373,14 +375,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</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}>
|
<View style={styles.genreContainer as ViewStyle}>
|
||||||
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
|
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
|
||||||
<React.Fragment key={index}>
|
<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 && (
|
{index < array.length - 1 && (
|
||||||
<Text style={styles.genreDot as TextStyle}>•</Text>
|
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}>•</Text>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
@ -395,15 +401,15 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isSaved ? "bookmark" : "bookmark-border"}
|
name={isSaved ? "bookmark" : "bookmark-border"}
|
||||||
size={24}
|
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"}
|
{isSaved ? "Saved" : "Save"}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.playButton as ViewStyle}
|
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (featuredContent) {
|
if (featuredContent) {
|
||||||
navigation.navigate('Streams', {
|
navigation.navigate('Streams', {
|
||||||
|
|
@ -413,8 +419,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="play-arrow" size={24} color={colors.black} />
|
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||||
<Text style={styles.playButtonText as TextStyle}>Play</Text>
|
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||||
|
Play
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -428,8 +436,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="info-outline" size={24} color={colors.white} />
|
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
|
||||||
<Text style={styles.infoButtonText as TextStyle}>Info</Text>
|
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||||
|
Info
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
@ -446,7 +456,6 @@ const styles = StyleSheet.create({
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
},
|
},
|
||||||
imageContainer: {
|
imageContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -468,7 +477,6 @@ const styles = StyleSheet.create({
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
|
@ -491,7 +499,6 @@ const styles = StyleSheet.create({
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
featuredTitleText: {
|
featuredTitleText: {
|
||||||
color: colors.highEmphasis,
|
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
|
@ -510,13 +517,11 @@ const styles = StyleSheet.create({
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
genreText: {
|
genreText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
},
|
},
|
||||||
genreDot: {
|
genreDot: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
|
|
@ -538,7 +543,6 @@ const styles = StyleSheet.create({
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 32,
|
paddingHorizontal: 32,
|
||||||
borderRadius: 30,
|
borderRadius: 30,
|
||||||
backgroundColor: colors.white,
|
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
|
@ -568,18 +572,15 @@ const styles = StyleSheet.create({
|
||||||
flex: undefined,
|
flex: undefined,
|
||||||
},
|
},
|
||||||
playButtonText: {
|
playButtonText: {
|
||||||
color: colors.black,
|
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
myListButtonText: {
|
myListButtonText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
infoButtonText: {
|
infoButtonText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';
|
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');
|
const { height } = Dimensions.get('window');
|
||||||
|
|
||||||
export const SkeletonCatalog = () => (
|
export const SkeletonCatalog = () => {
|
||||||
<View style={styles.catalogContainer}>
|
const { currentTheme } = useTheme();
|
||||||
<View style={styles.loadingPlaceholder}>
|
return (
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
<View style={styles.catalogContainer}>
|
||||||
|
<View style={[styles.loadingPlaceholder, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export const SkeletonFeatured = () => (
|
export const SkeletonFeatured = () => {
|
||||||
<View style={styles.featuredLoadingContainer}>
|
const { currentTheme } = useTheme();
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
return (
|
||||||
<Text style={styles.loadingText}>Loading featured content...</Text>
|
<View style={[styles.featuredLoadingContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
</View>
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
);
|
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading featured content...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
catalogContainer: {
|
catalogContainer: {
|
||||||
|
|
@ -29,7 +36,6 @@ const styles = StyleSheet.create({
|
||||||
height: 200,
|
height: 200,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
|
@ -37,28 +43,23 @@ const styles = StyleSheet.create({
|
||||||
height: height * 0.4,
|
height: height * 0.4,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
},
|
},
|
||||||
loadingText: {
|
loadingText: {
|
||||||
color: colors.textMuted,
|
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
skeletonBox: {
|
skeletonBox: {
|
||||||
backgroundColor: colors.elevation2,
|
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
skeletonFeatured: {
|
skeletonFeatured: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: height * 0.6,
|
height: height * 0.6,
|
||||||
backgroundColor: colors.elevation2,
|
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
skeletonPoster: {
|
skeletonPoster: {
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
marginHorizontal: 4,
|
marginHorizontal: 4,
|
||||||
borderRadius: 16,
|
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 { NuvioHeader } from '../components/NuvioHeader';
|
||||||
import { Stream } from '../types/streams';
|
import { Stream } from '../types/streams';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
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 screens with their proper types
|
||||||
import HomeScreen from '../screens/HomeScreen';
|
import HomeScreen from '../screens/HomeScreen';
|
||||||
|
|
@ -36,6 +38,7 @@ import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
|
||||||
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
||||||
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
||||||
import LogoSourceSettings from '../screens/LogoSourceSettings';
|
import LogoSourceSettings from '../screens/LogoSourceSettings';
|
||||||
|
import ThemeScreen from '../screens/ThemeScreen';
|
||||||
|
|
||||||
// Stack navigator types
|
// Stack navigator types
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
|
|
@ -92,6 +95,7 @@ export type RootStackParamList = {
|
||||||
TraktSettings: undefined;
|
TraktSettings: undefined;
|
||||||
PlayerSettings: undefined;
|
PlayerSettings: undefined;
|
||||||
LogoSourceSettings: undefined;
|
LogoSourceSettings: undefined;
|
||||||
|
ThemeSettings: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
@ -653,8 +657,7 @@ const MainTabs = () => {
|
||||||
|
|
||||||
// Stack Navigator
|
// Stack Navigator
|
||||||
const AppNavigator = () => {
|
const AppNavigator = () => {
|
||||||
// Always use dark mode
|
const { currentTheme } = useTheme();
|
||||||
const isDarkMode = true;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
|
|
@ -671,7 +674,7 @@ const AppNavigator = () => {
|
||||||
animation: 'none',
|
animation: 'none',
|
||||||
// Ensure content is not popping in and out
|
// Ensure content is not popping in and out
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -723,7 +726,7 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -738,7 +741,7 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -776,7 +779,7 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -791,7 +794,7 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -806,7 +809,7 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -821,7 +824,7 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -836,7 +839,22 @@ const AppNavigator = () => {
|
||||||
gestureDirection: 'horizontal',
|
gestureDirection: 'horizontal',
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
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 { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../styles';
|
|
||||||
import { catalogService, StreamingContent } from '../services/catalogService';
|
import { catalogService, StreamingContent } from '../services/catalogService';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import CategorySelector from '../components/discover/CategorySelector';
|
import CategorySelector from '../components/discover/CategorySelector';
|
||||||
|
|
@ -37,6 +37,7 @@ const DiscoverScreen = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const styles = useDiscoverStyles();
|
const styles = useDiscoverStyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
// Force consistent status bar settings
|
// Force consistent status bar settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -162,7 +163,7 @@ const DiscoverScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="search"
|
name="search"
|
||||||
size={24}
|
size={24}
|
||||||
color={colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -187,7 +188,7 @@ const DiscoverScreen = () => {
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
) : catalogs.length > 0 ? (
|
) : catalogs.length > 0 ? (
|
||||||
<CatalogsList
|
<CatalogsList
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import { Stream } from '../types/metadata';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { colors } from '../styles/colors';
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
|
|
@ -58,7 +57,9 @@ import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
||||||
import FeaturedContent from '../components/home/FeaturedContent';
|
import FeaturedContent from '../components/home/FeaturedContent';
|
||||||
import CatalogSection from '../components/home/CatalogSection';
|
import CatalogSection from '../components/home/CatalogSection';
|
||||||
import { SkeletonFeatured } from '../components/home/SkeletonLoaders';
|
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
|
// Define interfaces for our data
|
||||||
interface Category {
|
interface Category {
|
||||||
|
|
@ -86,6 +87,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
||||||
const translateY = useSharedValue(300);
|
const translateY = useSharedValue(300);
|
||||||
const opacity = useSharedValue(0);
|
const opacity = useSharedValue(0);
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
const SNAP_THRESHOLD = 100;
|
const SNAP_THRESHOLD = 100;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -126,12 +128,14 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
||||||
|
|
||||||
const overlayStyle = useAnimatedStyle(() => ({
|
const overlayStyle = useAnimatedStyle(() => ({
|
||||||
opacity: opacity.value,
|
opacity: opacity.value,
|
||||||
|
backgroundColor: currentTheme.colors.transparentDark,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const menuStyle = useAnimatedStyle(() => ({
|
const menuStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{ translateY: translateY.value }],
|
transform: [{ translateY: translateY.value }],
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
|
backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const menuOptions = [
|
const menuOptions = [
|
||||||
|
|
@ -157,8 +161,6 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
|
|
@ -170,20 +172,20 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
||||||
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
|
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
|
||||||
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
|
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
|
||||||
<GestureDetector gesture={gesture}>
|
<GestureDetector gesture={gesture}>
|
||||||
<Animated.View style={[styles.menuContainer, menuStyle, { backgroundColor }]}>
|
<Animated.View style={[styles.menuContainer, menuStyle]}>
|
||||||
<View style={styles.dragHandle} />
|
<View style={[styles.dragHandle, { backgroundColor: currentTheme.colors.transparentLight }]} />
|
||||||
<View style={styles.menuHeader}>
|
<View style={[styles.menuHeader, { borderBottomColor: currentTheme.colors.border }]}>
|
||||||
<ExpoImage
|
<ExpoImage
|
||||||
source={{ uri: item.poster }}
|
source={{ uri: item.poster }}
|
||||||
style={styles.menuPoster}
|
style={styles.menuPoster}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
<View style={styles.menuTitleContainer}>
|
<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}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.year && (
|
{item.year && (
|
||||||
<Text style={[styles.menuYear, { color: isDarkMode ? '#999999' : '#666666' }]}>
|
<Text style={[styles.menuYear, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
|
||||||
{item.year}
|
{item.year}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
@ -206,11 +208,11 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
|
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
|
||||||
size={24}
|
size={24}
|
||||||
color={colors.primary}
|
color={currentTheme.colors.primary}
|
||||||
/>
|
/>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.menuOptionText,
|
styles.menuOptionText,
|
||||||
{ color: isDarkMode ? '#FFFFFF' : '#000000' }
|
{ color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }
|
||||||
]}>
|
]}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -231,6 +233,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
||||||
const [isWatched, setIsWatched] = useState(false);
|
const [isWatched, setIsWatched] = useState(false);
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
const handleLongPress = useCallback(() => {
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
|
|
@ -306,22 +309,22 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{(!imageLoaded || imageError) && (
|
{(!imageLoaded || imageError) && (
|
||||||
<View style={[styles.loadingOverlay, { backgroundColor: colors.elevation2 }]}>
|
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||||
{!imageError ? (
|
{!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>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isWatched && (
|
{isWatched && (
|
||||||
<View style={styles.watchedIndicator}>
|
<View style={styles.watchedIndicator}>
|
||||||
<MaterialIcons name="check-circle" size={22} color={colors.success} />
|
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{localItem.inLibrary && (
|
{localItem.inLibrary && (
|
||||||
<View style={styles.libraryBadge}>
|
<View style={styles.libraryBadge}>
|
||||||
<MaterialIcons name="bookmark" size={16} color={colors.white} />
|
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -344,17 +347,21 @@ const SAMPLE_CATEGORIES: Category[] = [
|
||||||
{ id: 'channel', name: 'Channels' },
|
{ id: 'channel', name: 'Channels' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SkeletonCatalog = () => (
|
const SkeletonCatalog = () => {
|
||||||
<View style={styles.catalogContainer}>
|
const { currentTheme } = useTheme();
|
||||||
<View style={styles.loadingPlaceholder}>
|
return (
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
<View style={styles.catalogContainer}>
|
||||||
|
<View style={styles.loadingPlaceholder}>
|
||||||
|
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
const HomeScreen = () => {
|
const HomeScreen = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
const continueWatchingRef = useRef<ContinueWatchingRef>(null);
|
const continueWatchingRef = useRef<ContinueWatchingRef>(null);
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
|
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
|
||||||
|
|
@ -436,9 +443,9 @@ const HomeScreen = () => {
|
||||||
// Only run cleanup when component unmounts completely, not on unfocus
|
// Only run cleanup when component unmounts completely, not on unfocus
|
||||||
return () => {
|
return () => {
|
||||||
StatusBar.setTranslucent(false);
|
StatusBar.setTranslucent(false);
|
||||||
StatusBar.setBackgroundColor(colors.darkBackground);
|
StatusBar.setBackgroundColor(currentTheme.colors.darkBackground);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [currentTheme.colors.darkBackground]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.addListener('beforeRemove', () => {});
|
navigation.addListener('beforeRemove', () => {});
|
||||||
|
|
@ -531,22 +538,22 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
if (isLoading && !isRefreshing) {
|
if (isLoading && !isRefreshing) {
|
||||||
return (
|
return (
|
||||||
<View style={homeStyles.container}>
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
barStyle="light-content"
|
barStyle="light-content"
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
translucent
|
translucent
|
||||||
/>
|
/>
|
||||||
<View style={homeStyles.loadingMainContainer}>
|
<View style={styles.loadingMainContainer}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
<Text style={homeStyles.loadingText}>Loading your content...</Text>
|
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading your content...</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={homeStyles.container}>
|
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
barStyle="light-content"
|
barStyle="light-content"
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
|
|
@ -557,12 +564,12 @@ const HomeScreen = () => {
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
tintColor={colors.primary}
|
tintColor={currentTheme.colors.primary}
|
||||||
colors={[colors.primary, colors.secondary]}
|
colors={[currentTheme.colors.primary, currentTheme.colors.secondary]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
homeStyles.scrollContent,
|
styles.scrollContent,
|
||||||
{ paddingTop: Platform.OS === 'ios' ? 39 : 90 }
|
{ paddingTop: Platform.OS === 'ios' ? 39 : 90 }
|
||||||
]}
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
|
@ -594,17 +601,17 @@ const HomeScreen = () => {
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
!catalogsLoading && (
|
!catalogsLoading && (
|
||||||
<View style={homeStyles.emptyCatalog}>
|
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
<MaterialIcons name="movie-filter" size={40} color={colors.textDark} />
|
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
||||||
<Text style={{ color: colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||||
No content available
|
No content available
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={homeStyles.addCatalogButton}
|
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => navigation.navigate('Settings')}
|
onPress={() => navigation.navigate('Settings')}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="add-circle" size={20} color={colors.white} />
|
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||||
<Text style={homeStyles.addCatalogButtonText}>Add Catalogs</Text>
|
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
@ -620,11 +627,44 @@ const POSTER_WIDTH = (width - 50) / 3;
|
||||||
const styles = StyleSheet.create<any>({
|
const styles = StyleSheet.create<any>({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
paddingBottom: 40,
|
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: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
@ -661,7 +701,6 @@ const styles = StyleSheet.create<any>({
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
featuredTitle: {
|
featuredTitle: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
|
|
@ -679,13 +718,11 @@ const styles = StyleSheet.create<any>({
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
genreText: {
|
genreText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
},
|
},
|
||||||
genreDot: {
|
genreDot: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
|
|
@ -707,7 +744,7 @@ const styles = StyleSheet.create<any>({
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
paddingHorizontal: 32,
|
paddingHorizontal: 32,
|
||||||
borderRadius: 30,
|
borderRadius: 30,
|
||||||
backgroundColor: colors.white,
|
backgroundColor: '#FFFFFF',
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
|
@ -737,18 +774,16 @@ const styles = StyleSheet.create<any>({
|
||||||
flex: null,
|
flex: null,
|
||||||
},
|
},
|
||||||
playButtonText: {
|
playButtonText: {
|
||||||
color: colors.black,
|
color: '#000000',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
myListButtonText: {
|
myListButtonText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
infoButtonText: {
|
infoButtonText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
@ -770,7 +805,6 @@ const styles = StyleSheet.create<any>({
|
||||||
catalogTitle: {
|
catalogTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
color: colors.highEmphasis,
|
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
|
|
@ -786,13 +820,11 @@ const styles = StyleSheet.create<any>({
|
||||||
seeAllButton: {
|
seeAllButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
},
|
},
|
||||||
seeAllText: {
|
seeAllText: {
|
||||||
color: colors.primary,
|
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
marginRight: 4,
|
marginRight: 4,
|
||||||
|
|
@ -841,28 +873,18 @@ const styles = StyleSheet.create<any>({
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginLeft: 3,
|
marginLeft: 3,
|
||||||
},
|
},
|
||||||
emptyCatalog: {
|
|
||||||
padding: 32,
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
margin: 16,
|
|
||||||
borderRadius: 16,
|
|
||||||
},
|
|
||||||
skeletonBox: {
|
skeletonBox: {
|
||||||
backgroundColor: colors.elevation2,
|
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
skeletonFeatured: {
|
skeletonFeatured: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: height * 0.6,
|
height: height * 0.6,
|
||||||
backgroundColor: colors.elevation2,
|
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
},
|
},
|
||||||
skeletonPoster: {
|
skeletonPoster: {
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
marginHorizontal: 4,
|
marginHorizontal: 4,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
},
|
},
|
||||||
|
|
@ -888,7 +910,6 @@ const styles = StyleSheet.create<any>({
|
||||||
modalOverlay: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
backgroundColor: colors.transparentDark,
|
|
||||||
},
|
},
|
||||||
modalOverlayPressable: {
|
modalOverlayPressable: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -896,7 +917,6 @@ const styles = StyleSheet.create<any>({
|
||||||
dragHandle: {
|
dragHandle: {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 4,
|
height: 4,
|
||||||
backgroundColor: colors.transparentLight,
|
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
|
|
@ -908,7 +928,7 @@ const styles = StyleSheet.create<any>({
|
||||||
paddingBottom: Platform.select({ ios: 40, android: 24 }),
|
paddingBottom: Platform.select({ ios: 40, android: 24 }),
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
shadowColor: colors.black,
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: -3 },
|
shadowOffset: { width: 0, height: -3 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 5,
|
shadowRadius: 5,
|
||||||
|
|
@ -922,7 +942,6 @@ const styles = StyleSheet.create<any>({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: colors.border,
|
|
||||||
},
|
},
|
||||||
menuPoster: {
|
menuPoster: {
|
||||||
width: 60,
|
width: 60,
|
||||||
|
|
@ -962,7 +981,7 @@ const styles = StyleSheet.create<any>({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
backgroundColor: colors.transparentDark,
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
},
|
},
|
||||||
|
|
@ -970,7 +989,7 @@ const styles = StyleSheet.create<any>({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 8,
|
left: 8,
|
||||||
backgroundColor: colors.transparentDark,
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
},
|
},
|
||||||
|
|
@ -996,7 +1015,6 @@ const styles = StyleSheet.create<any>({
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
featuredTitleText: {
|
featuredTitleText: {
|
||||||
color: colors.highEmphasis,
|
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
|
@ -1006,42 +1024,10 @@ const styles = StyleSheet.create<any>({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingHorizontal: 16,
|
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: {
|
loadingPlaceholder: {
|
||||||
height: 200,
|
height: 200,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
|
@ -1049,7 +1035,6 @@ const styles = StyleSheet.create<any>({
|
||||||
height: height * 0.4,
|
height: height * 0.4,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import {
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../styles';
|
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
@ -25,6 +24,7 @@ import type { StreamingContent } from '../services/catalogService';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface LibraryItem extends StreamingContent {
|
interface LibraryItem extends StreamingContent {
|
||||||
|
|
@ -38,6 +38,7 @@ const SkeletonLoader = () => {
|
||||||
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const itemWidth = (width - 48) / 2;
|
const itemWidth = (width - 48) / 2;
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const pulse = RNAnimated.loop(
|
const pulse = RNAnimated.loop(
|
||||||
|
|
@ -68,13 +69,13 @@ const SkeletonLoader = () => {
|
||||||
<RNAnimated.View
|
<RNAnimated.View
|
||||||
style={[
|
style={[
|
||||||
styles.posterContainer,
|
styles.posterContainer,
|
||||||
{ opacity, backgroundColor: colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<RNAnimated.View
|
<RNAnimated.View
|
||||||
style={[
|
style={[
|
||||||
styles.skeletonTitle,
|
styles.skeletonTitle,
|
||||||
{ opacity, backgroundColor: colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -99,6 +100,7 @@ const LibraryScreen = () => {
|
||||||
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
||||||
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
|
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
// Force consistent status bar settings
|
// Force consistent status bar settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -157,7 +159,7 @@ const LibraryScreen = () => {
|
||||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.posterContainer}>
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||||
style={styles.poster}
|
style={styles.poster}
|
||||||
|
|
@ -169,7 +171,7 @@ const LibraryScreen = () => {
|
||||||
style={styles.posterGradient}
|
style={styles.posterGradient}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={styles.itemTitle}
|
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
|
@ -186,7 +188,7 @@ const LibraryScreen = () => {
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.progressBar,
|
styles.progressBar,
|
||||||
{ width: `${item.progress * 100}%` }
|
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -196,10 +198,10 @@ const LibraryScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="live-tv"
|
name="live-tv"
|
||||||
size={14}
|
size={14}
|
||||||
color={colors.white}
|
color={currentTheme.colors.white}
|
||||||
style={{ marginRight: 4 }}
|
style={{ marginRight: 4 }}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.badgeText}>Series</Text>
|
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>Series</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -212,7 +214,8 @@ const LibraryScreen = () => {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
isActive && styles.filterButtonActive,
|
isActive && { backgroundColor: currentTheme.colors.primary },
|
||||||
|
{ shadowColor: currentTheme.colors.black }
|
||||||
]}
|
]}
|
||||||
onPress={() => setFilter(filterType)}
|
onPress={() => setFilter(filterType)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -220,13 +223,14 @@ const LibraryScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={iconName}
|
name={iconName}
|
||||||
size={22}
|
size={22}
|
||||||
color={isActive ? colors.white : colors.mediumGray}
|
color={isActive ? currentTheme.colors.white : currentTheme.colors.mediumGray}
|
||||||
style={styles.filterIcon}
|
style={styles.filterIcon}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.filterText,
|
styles.filterText,
|
||||||
isActive && styles.filterTextActive
|
{ color: currentTheme.colors.mediumGray },
|
||||||
|
isActive && { color: currentTheme.colors.white, fontWeight: '600' }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
@ -240,20 +244,20 @@ const LibraryScreen = () => {
|
||||||
const headerHeight = headerBaseHeight + topSpacing;
|
const headerHeight = headerBaseHeight + topSpacing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
{/* Fixed position header background to prevent shifts */}
|
{/* 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 }}>
|
<View style={{ flex: 1 }}>
|
||||||
{/* Header Section with proper top spacing */}
|
{/* Header Section with proper top spacing */}
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||||
<View style={styles.headerContent}>
|
<View style={styles.headerContent}>
|
||||||
<Text style={styles.headerTitle}>Library</Text>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<View style={styles.contentContainer}>
|
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<View style={styles.filtersContainer}>
|
<View style={styles.filtersContainer}>
|
||||||
{renderFilter('all', 'All', 'apps')}
|
{renderFilter('all', 'All', 'apps')}
|
||||||
{renderFilter('movies', 'Movies', 'movie')}
|
{renderFilter('movies', 'Movies', 'movie')}
|
||||||
|
|
@ -267,19 +271,22 @@ const LibraryScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="video-library"
|
name="video-library"
|
||||||
size={80}
|
size={80}
|
||||||
color={colors.mediumGray}
|
color={currentTheme.colors.mediumGray}
|
||||||
style={{ opacity: 0.7 }}
|
style={{ opacity: 0.7 }}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.emptyText}>Your library is empty</Text>
|
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>Your library is empty</Text>
|
||||||
<Text style={styles.emptySubtext}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
Add content to your library to keep track of what you're watching
|
Add content to your library to keep track of what you're watching
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.exploreButton}
|
style={[styles.exploreButton, {
|
||||||
|
backgroundColor: currentTheme.colors.primary,
|
||||||
|
shadowColor: currentTheme.colors.black
|
||||||
|
}]}
|
||||||
onPress={() => navigation.navigate('Discover')}
|
onPress={() => navigation.navigate('Discover')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.exploreButtonText}>Explore Content</Text>
|
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Explore Content</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -306,19 +313,16 @@ const LibraryScreen = () => {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
},
|
},
|
||||||
headerBackground: {
|
headerBackground: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
|
|
@ -335,7 +339,6 @@ const styles = StyleSheet.create({
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
color: colors.white,
|
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
},
|
},
|
||||||
filtersContainer: {
|
filtersContainer: {
|
||||||
|
|
@ -355,26 +358,17 @@ const styles = StyleSheet.create({
|
||||||
marginHorizontal: 4,
|
marginHorizontal: 4,
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||||
shadowColor: colors.black,
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
filterButtonActive: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
filterIcon: {
|
filterIcon: {
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
filterText: {
|
filterText: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: colors.mediumGray,
|
|
||||||
},
|
|
||||||
filterTextActive: {
|
|
||||||
fontWeight: '600',
|
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
listContainer: {
|
listContainer: {
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
|
|
@ -400,7 +394,6 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||||
aspectRatio: 2/3,
|
aspectRatio: 2/3,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
shadowColor: colors.black,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.2,
|
shadowOpacity: 0.2,
|
||||||
shadowRadius: 8,
|
shadowRadius: 8,
|
||||||
|
|
@ -428,7 +421,6 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
progressBar: {
|
progressBar: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
},
|
||||||
badgeContainer: {
|
badgeContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -442,14 +434,12 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
badgeText: {
|
badgeText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
itemTitle: {
|
itemTitle: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.white,
|
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
textShadowOffset: { width: 0, height: 1 },
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
|
|
@ -477,29 +467,24 @@ const styles = StyleSheet.create({
|
||||||
emptyText: {
|
emptyText: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.white,
|
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
emptySubtext: {
|
emptySubtext: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: colors.mediumGray,
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
exploreButton: {
|
exploreButton: {
|
||||||
backgroundColor: colors.primary,
|
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
shadowColor: colors.black,
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.2,
|
shadowOpacity: 0.2,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
},
|
},
|
||||||
exploreButtonText: {
|
exploreButtonText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import {
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { colors } from '../styles';
|
|
||||||
import { catalogService, StreamingContent } from '../services/catalogService';
|
import { catalogService, StreamingContent } from '../services/catalogService';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
@ -42,6 +41,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
|
const HORIZONTAL_ITEM_WIDTH = width * 0.3;
|
||||||
|
|
@ -57,6 +57,7 @@ const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
const SkeletonLoader = () => {
|
||||||
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const pulse = RNAnimated.loop(
|
const pulse = RNAnimated.loop(
|
||||||
|
|
@ -84,12 +85,24 @@ const SkeletonLoader = () => {
|
||||||
|
|
||||||
const renderSkeletonItem = () => (
|
const renderSkeletonItem = () => (
|
||||||
<View style={styles.skeletonVerticalItem}>
|
<View style={styles.skeletonVerticalItem}>
|
||||||
<RNAnimated.View style={[styles.skeletonPoster, { opacity }]} />
|
<RNAnimated.View style={[
|
||||||
|
styles.skeletonPoster,
|
||||||
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
|
]} />
|
||||||
<View style={styles.skeletonItemDetails}>
|
<View style={styles.skeletonItemDetails}>
|
||||||
<RNAnimated.View style={[styles.skeletonTitle, { opacity }]} />
|
<RNAnimated.View style={[
|
||||||
|
styles.skeletonTitle,
|
||||||
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
|
]} />
|
||||||
<View style={styles.skeletonMetaRow}>
|
<View style={styles.skeletonMetaRow}>
|
||||||
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
|
<RNAnimated.View style={[
|
||||||
<RNAnimated.View style={[styles.skeletonMeta, { opacity }]} />
|
styles.skeletonMeta,
|
||||||
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
|
]} />
|
||||||
|
<RNAnimated.View style={[
|
||||||
|
styles.skeletonMeta,
|
||||||
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
|
]} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -100,7 +113,10 @@ const SkeletonLoader = () => {
|
||||||
{[...Array(5)].map((_, index) => (
|
{[...Array(5)].map((_, index) => (
|
||||||
<View key={index}>
|
<View key={index}>
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<RNAnimated.View style={[styles.skeletonSectionHeader, { opacity }]} />
|
<RNAnimated.View style={[
|
||||||
|
styles.skeletonSectionHeader,
|
||||||
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
|
]} />
|
||||||
)}
|
)}
|
||||||
{renderSkeletonItem()}
|
{renderSkeletonItem()}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -116,6 +132,7 @@ const SimpleSearchAnimation = () => {
|
||||||
// Simple animation values that work reliably
|
// Simple animation values that work reliably
|
||||||
const spinAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const spinAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
const fadeAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const fadeAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Rotation animation
|
// Rotation animation
|
||||||
|
|
@ -161,15 +178,15 @@ const SimpleSearchAnimation = () => {
|
||||||
<View style={styles.simpleAnimationContent}>
|
<View style={styles.simpleAnimationContent}>
|
||||||
<RNAnimated.View style={[
|
<RNAnimated.View style={[
|
||||||
styles.spinnerContainer,
|
styles.spinnerContainer,
|
||||||
{ transform: [{ rotate: spin }] }
|
{ transform: [{ rotate: spin }], backgroundColor: currentTheme.colors.primary }
|
||||||
]}>
|
]}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="search"
|
name="search"
|
||||||
size={32}
|
size={32}
|
||||||
color={colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</RNAnimated.View>
|
</RNAnimated.View>
|
||||||
<Text style={styles.simpleAnimationText}>Searching</Text>
|
<Text style={[styles.simpleAnimationText, { color: currentTheme.colors.white }]}>Searching</Text>
|
||||||
</View>
|
</View>
|
||||||
</RNAnimated.View>
|
</RNAnimated.View>
|
||||||
);
|
);
|
||||||
|
|
@ -186,6 +203,7 @@ const SearchScreen = () => {
|
||||||
const [showRecent, setShowRecent] = useState(true);
|
const [showRecent, setShowRecent] = useState(true);
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
// Animation values
|
// Animation values
|
||||||
const searchBarWidth = useSharedValue(width - 32);
|
const searchBarWidth = useSharedValue(width - 32);
|
||||||
|
|
@ -348,7 +366,7 @@ const SearchScreen = () => {
|
||||||
style={styles.recentSearchesContainer}
|
style={styles.recentSearchesContainer}
|
||||||
entering={FadeIn.duration(300)}
|
entering={FadeIn.duration(300)}
|
||||||
>
|
>
|
||||||
<Text style={styles.carouselTitle}>
|
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||||
Recent Searches
|
Recent Searches
|
||||||
</Text>
|
</Text>
|
||||||
{recentSearches.map((search, index) => (
|
{recentSearches.map((search, index) => (
|
||||||
|
|
@ -364,10 +382,10 @@ const SearchScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="history"
|
name="history"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.lightGray}
|
color={currentTheme.colors.lightGray}
|
||||||
style={styles.recentSearchIcon}
|
style={styles.recentSearchIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.recentSearchText}>
|
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
|
||||||
{search}
|
{search}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -380,7 +398,7 @@ const SearchScreen = () => {
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
style={styles.recentSearchDeleteButton}
|
style={styles.recentSearchDeleteButton}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="close" size={16} color={colors.lightGray} />
|
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</AnimatedTouchable>
|
</AnimatedTouchable>
|
||||||
))}
|
))}
|
||||||
|
|
@ -398,7 +416,10 @@ const SearchScreen = () => {
|
||||||
entering={FadeIn.duration(500).delay(index * 100)}
|
entering={FadeIn.duration(500).delay(index * 100)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={styles.horizontalItemPosterContainer}>
|
<View style={[styles.horizontalItemPosterContainer, {
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
borderColor: 'rgba(255,255,255,0.05)'
|
||||||
|
}]}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
||||||
style={styles.horizontalItemPoster}
|
style={styles.horizontalItemPoster}
|
||||||
|
|
@ -406,23 +427,29 @@ const SearchScreen = () => {
|
||||||
transition={300}
|
transition={300}
|
||||||
/>
|
/>
|
||||||
<View style={styles.itemTypeContainer}>
|
<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>
|
</View>
|
||||||
{item.imdbRating && (
|
{item.imdbRating && (
|
||||||
<View style={styles.ratingContainer}>
|
<View style={styles.ratingContainer}>
|
||||||
<MaterialIcons name="star" size={12} color="#FFC107" />
|
<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>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={styles.horizontalItemTitle}
|
style={[styles.horizontalItemTitle, { color: currentTheme.colors.white }]}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.year && (
|
{item.year && (
|
||||||
<Text style={styles.yearText}>{item.year}</Text>
|
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray }]}>
|
||||||
|
{item.year}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</AnimatedTouchable>
|
</AnimatedTouchable>
|
||||||
);
|
);
|
||||||
|
|
@ -445,7 +472,7 @@ const SearchScreen = () => {
|
||||||
const headerHeight = headerBaseHeight + topSpacing + 60;
|
const headerHeight = headerBaseHeight + topSpacing + 60;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
barStyle="light-content"
|
barStyle="light-content"
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
|
|
@ -453,56 +480,68 @@ const SearchScreen = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Fixed position header background to prevent shifts */}
|
{/* 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 }}>
|
<View style={{ flex: 1 }}>
|
||||||
{/* Header Section with proper top spacing */}
|
{/* Header Section with proper top spacing */}
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||||
<Text style={styles.headerTitle}>Search</Text>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Search</Text>
|
||||||
<View style={[
|
<View style={styles.searchBarContainer}>
|
||||||
styles.searchBar,
|
<View style={[
|
||||||
{
|
styles.searchBarWrapper,
|
||||||
backgroundColor: colors.darkGray,
|
{ width: '100%' }
|
||||||
borderColor: 'transparent',
|
]}>
|
||||||
}
|
<View style={[
|
||||||
]}>
|
styles.searchBar,
|
||||||
<MaterialIcons
|
{
|
||||||
name="search"
|
backgroundColor: currentTheme.colors.elevation2,
|
||||||
size={24}
|
borderColor: 'rgba(255,255,255,0.1)',
|
||||||
color={colors.lightGray}
|
borderWidth: 1,
|
||||||
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 }}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="close"
|
name="search"
|
||||||
size={20}
|
size={24}
|
||||||
color={colors.lightGray}
|
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>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<View style={styles.contentContainer}>
|
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
{searching ? (
|
{searching ? (
|
||||||
<SimpleSearchAnimation />
|
<SimpleSearchAnimation />
|
||||||
) : searched && !hasResultsToShow ? (
|
) : searched && !hasResultsToShow ? (
|
||||||
|
|
@ -513,12 +552,12 @@ const SearchScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="search-off"
|
name="search-off"
|
||||||
size={64}
|
size={64}
|
||||||
color={colors.lightGray}
|
color={currentTheme.colors.lightGray}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.emptyText}>
|
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||||
No results found
|
No results found
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.emptySubtext}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||||
Try different keywords or check your spelling
|
Try different keywords or check your spelling
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -538,7 +577,9 @@ const SearchScreen = () => {
|
||||||
style={styles.carouselContainer}
|
style={styles.carouselContainer}
|
||||||
entering={FadeIn.duration(300)}
|
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
|
<FlatList
|
||||||
data={movieResults}
|
data={movieResults}
|
||||||
renderItem={renderHorizontalItem}
|
renderItem={renderHorizontalItem}
|
||||||
|
|
@ -555,7 +596,9 @@ const SearchScreen = () => {
|
||||||
style={styles.carouselContainer}
|
style={styles.carouselContainer}
|
||||||
entering={FadeIn.duration(300).delay(100)}
|
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
|
<FlatList
|
||||||
data={seriesResults}
|
data={seriesResults}
|
||||||
renderItem={renderHorizontalItem}
|
renderItem={renderHorizontalItem}
|
||||||
|
|
@ -578,19 +621,16 @@ const SearchScreen = () => {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.black,
|
|
||||||
},
|
},
|
||||||
headerBackground: {
|
headerBackground: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
backgroundColor: colors.black,
|
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.black,
|
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
|
@ -603,26 +643,26 @@ const styles = StyleSheet.create({
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
color: colors.white,
|
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
searchBarContainer: {
|
searchBarContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
height: 48,
|
||||||
},
|
},
|
||||||
searchBarWrapper: {
|
searchBarWrapper: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
height: 48,
|
||||||
},
|
},
|
||||||
searchBar: {
|
searchBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
height: 48,
|
height: '100%',
|
||||||
backgroundColor: colors.darkGray,
|
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
|
|
@ -632,13 +672,6 @@ const styles = StyleSheet.create({
|
||||||
shadowRadius: 3.84,
|
shadowRadius: 3.84,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
},
|
},
|
||||||
backButton: {
|
|
||||||
marginRight: 10,
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
searchIcon: {
|
searchIcon: {
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
|
|
@ -646,7 +679,6 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
clearButton: {
|
clearButton: {
|
||||||
padding: 4,
|
padding: 4,
|
||||||
|
|
@ -664,7 +696,6 @@ const styles = StyleSheet.create({
|
||||||
carouselTitle: {
|
carouselTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: colors.white,
|
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
|
@ -681,10 +712,8 @@ const styles = StyleSheet.create({
|
||||||
height: HORIZONTAL_POSTER_HEIGHT,
|
height: HORIZONTAL_POSTER_HEIGHT,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(255,255,255,0.05)',
|
|
||||||
},
|
},
|
||||||
horizontalItemPoster: {
|
horizontalItemPoster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -695,11 +724,9 @@ const styles = StyleSheet.create({
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
yearText: {
|
yearText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: colors.mediumGray,
|
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
recentSearchesContainer: {
|
recentSearchesContainer: {
|
||||||
|
|
@ -723,7 +750,6 @@ const styles = StyleSheet.create({
|
||||||
recentSearchText: {
|
recentSearchText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
recentSearchDeleteButton: {
|
recentSearchDeleteButton: {
|
||||||
padding: 4,
|
padding: 4,
|
||||||
|
|
@ -736,7 +762,6 @@ const styles = StyleSheet.create({
|
||||||
loadingText: {
|
loadingText: {
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -749,13 +774,11 @@ const styles = StyleSheet.create({
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
emptySubtext: {
|
emptySubtext: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
color: colors.lightGray,
|
|
||||||
},
|
},
|
||||||
skeletonContainer: {
|
skeletonContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -772,7 +795,6 @@ const styles = StyleSheet.create({
|
||||||
width: POSTER_WIDTH,
|
width: POSTER_WIDTH,
|
||||||
height: POSTER_HEIGHT,
|
height: POSTER_HEIGHT,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
},
|
},
|
||||||
skeletonItemDetails: {
|
skeletonItemDetails: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -788,19 +810,16 @@ const styles = StyleSheet.create({
|
||||||
height: 20,
|
height: 20,
|
||||||
width: '80%',
|
width: '80%',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
skeletonMeta: {
|
skeletonMeta: {
|
||||||
height: 14,
|
height: 14,
|
||||||
width: '30%',
|
width: '30%',
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
skeletonSectionHeader: {
|
skeletonSectionHeader: {
|
||||||
height: 24,
|
height: 24,
|
||||||
width: '40%',
|
width: '40%',
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
|
|
@ -814,7 +833,6 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
itemTypeText: {
|
itemTypeText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
|
@ -830,7 +848,6 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
ratingText: {
|
ratingText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
marginLeft: 2,
|
marginLeft: 2,
|
||||||
|
|
@ -847,7 +864,6 @@ const styles = StyleSheet.create({
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
borderRadius: 32,
|
borderRadius: 32,
|
||||||
backgroundColor: colors.primary,
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
|
|
@ -861,7 +877,6 @@ const styles = StyleSheet.create({
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
simpleAnimationText: {
|
simpleAnimationText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { Picker } from '@react-native-picker/picker';
|
import { Picker } from '@react-native-picker/picker';
|
||||||
import { colors } from '../styles/colors';
|
|
||||||
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { stremioService } from '../services/stremioService';
|
import { stremioService } from '../services/stremioService';
|
||||||
import { useCatalogContext } from '../contexts/CatalogContext';
|
import { useCatalogContext } from '../contexts/CatalogContext';
|
||||||
import { useTraktContext } from '../contexts/TraktContext';
|
import { useTraktContext } from '../contexts/TraktContext';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { catalogService, DataSource } from '../services/catalogService';
|
import { catalogService, DataSource } from '../services/catalogService';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
|
@ -39,24 +39,28 @@ interface SettingsCardProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => (
|
const SettingsCard: React.FC<SettingsCardProps> = ({ children, isDarkMode, title }) => {
|
||||||
<View style={[styles.cardContainer]}>
|
const { currentTheme } = useTheme();
|
||||||
{title && (
|
|
||||||
<Text style={[
|
return (
|
||||||
styles.cardTitle,
|
<View style={[styles.cardContainer]}>
|
||||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
{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()}
|
{children}
|
||||||
</Text>
|
</View>
|
||||||
)}
|
|
||||||
<View style={[
|
|
||||||
styles.card,
|
|
||||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
|
||||||
]}>
|
|
||||||
{children}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
interface SettingItemProps {
|
interface SettingItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -79,6 +83,8 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
badge
|
badge
|
||||||
}) => {
|
}) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -93,21 +99,21 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
styles.settingIconContainer,
|
styles.settingIconContainer,
|
||||||
{ backgroundColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)' }
|
{ 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>
|
||||||
<View style={styles.settingContent}>
|
<View style={styles.settingContent}>
|
||||||
<View style={styles.settingTextContainer}>
|
<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}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text style={[styles.settingDescription, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
|
<Text style={[styles.settingDescription, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{badge && (
|
{badge && (
|
||||||
<View style={[styles.badge, { backgroundColor: colors.primary }]}>
|
<View style={[styles.badge, { backgroundColor: currentTheme.colors.primary }]}>
|
||||||
<Text style={styles.badgeText}>{badge}</Text>
|
<Text style={styles.badgeText}>{badge}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -126,6 +132,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { lastUpdate } = useCatalogContext();
|
const { lastUpdate } = useCatalogContext();
|
||||||
const { isAuthenticated, userProfile } = useTraktContext();
|
const { isAuthenticated, userProfile } = useTraktContext();
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
// States for dynamic content
|
// States for dynamic content
|
||||||
|
|
@ -229,8 +236,8 @@ const SettingsScreen: React.FC = () => {
|
||||||
<Switch
|
<Switch
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
trackColor={{ false: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', true: colors.primary }}
|
trackColor={{ false: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)', true: currentTheme.colors.primary }}
|
||||||
thumbColor={Platform.OS === 'android' ? (value ? colors.white : colors.white) : ''}
|
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)'}
|
ios_backgroundColor={isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -257,22 +264,22 @@ const SettingsScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
{ backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
|
||||||
]}>
|
]}>
|
||||||
{/* Fixed position header background to prevent shifts */}
|
{/* Fixed position header background to prevent shifts */}
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.headerBackground,
|
styles.headerBackground,
|
||||||
{ height: headerHeight, backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
{ height: headerHeight, backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
|
||||||
]} />
|
]} />
|
||||||
|
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
{/* Header Section with proper top spacing */}
|
{/* Header Section with proper top spacing */}
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
<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
|
Settings
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
|
<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>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -399,25 +406,37 @@ const SettingsScreen: React.FC = () => {
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.selectorButton,
|
styles.selectorButton,
|
||||||
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
|
discoverDataSource === DataSource.STREMIO_ADDONS && {
|
||||||
|
backgroundColor: currentTheme.colors.primary
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
|
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.selectorText,
|
styles.selectorText,
|
||||||
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
|
{ color: currentTheme.colors.mediumEmphasis },
|
||||||
|
discoverDataSource === DataSource.STREMIO_ADDONS && {
|
||||||
|
color: currentTheme.colors.white,
|
||||||
|
fontWeight: '600'
|
||||||
|
}
|
||||||
]}>Addons</Text>
|
]}>Addons</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.selectorButton,
|
styles.selectorButton,
|
||||||
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
|
discoverDataSource === DataSource.TMDB && {
|
||||||
|
backgroundColor: currentTheme.colors.primary
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
|
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.selectorText,
|
styles.selectorText,
|
||||||
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
|
{ color: currentTheme.colors.mediumEmphasis },
|
||||||
|
discoverDataSource === DataSource.TMDB && {
|
||||||
|
color: currentTheme.colors.white,
|
||||||
|
fontWeight: '600'
|
||||||
|
}
|
||||||
]}>TMDB</Text>
|
]}>TMDB</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -425,8 +444,32 @@ const SettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</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}>
|
<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
|
Version 1.0.0
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -597,17 +640,9 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||||
},
|
},
|
||||||
selectorButtonActive: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
selectorText: {
|
selectorText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
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 {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -15,12 +15,17 @@ import {
|
||||||
Keyboard,
|
Keyboard,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
Switch,
|
Switch,
|
||||||
|
Image,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
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 { logger } from '../utils/logger';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_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 [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
const apiKeyInputRef = useRef<TextInput>(null);
|
const apiKeyInputRef = useRef<TextInput>(null);
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.log('[TMDBSettingsScreen] Component mounted');
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
<Text style={styles.loadingText}>Loading Settings...</Text>
|
<Text style={styles.loadingText}>Loading Settings...</Text>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
@ -237,42 +462,43 @@ const TMDBSettingsScreen = () => {
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
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>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.headerTitle}>TMDb Settings</Text>
|
<Text style={styles.title}>TMDb Settings</Text>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.content}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<View style={styles.switchCard}>
|
<View style={styles.switchCard}>
|
||||||
<View style={styles.switchRow}>
|
<View style={styles.switchTextContainer}>
|
||||||
<Text style={styles.switchLabel}>Use Custom TMDb API Key</Text>
|
<Text style={styles.switchTitle}>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>
|
</View>
|
||||||
<Text style={styles.switchDescription}>
|
<Switch
|
||||||
Enable to use your own TMDb API key instead of the built-in one.
|
value={useCustomKey}
|
||||||
Using your own API key may provide better performance and higher rate limits.
|
onValueChange={toggleUseCustomKey}
|
||||||
</Text>
|
trackColor={{ false: currentTheme.colors.lightGray, true: currentTheme.colors.accentLight }}
|
||||||
|
thumbColor={Platform.OS === 'android' ? currentTheme.colors.primary : ''}
|
||||||
|
ios_backgroundColor={currentTheme.colors.lightGray}
|
||||||
|
/>
|
||||||
</View>
|
</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 && (
|
{useCustomKey && (
|
||||||
<>
|
<>
|
||||||
<View style={styles.statusCard}>
|
<View style={styles.statusCard}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isKeySet ? "check-circle" : "error-outline"}
|
name={isKeySet ? "check-circle" : "error-outline"}
|
||||||
size={28}
|
size={28}
|
||||||
color={isKeySet ? colors.success : colors.warning}
|
color={isKeySet ? currentTheme.colors.success : currentTheme.colors.warning}
|
||||||
style={styles.statusIcon}
|
style={styles.statusIconContainer}
|
||||||
/>
|
/>
|
||||||
<View style={styles.statusTextContainer}>
|
<View style={styles.statusTextContainer}>
|
||||||
<Text style={styles.statusTitle}>
|
<Text style={styles.statusTitle}>
|
||||||
|
|
@ -287,8 +513,8 @@ const TMDBSettingsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.sectionTitle}>API Key</Text>
|
<Text style={styles.cardTitle}>API Key</Text>
|
||||||
<View style={styles.inputWrapper}>
|
<View style={styles.inputContainer}>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={apiKeyInputRef}
|
ref={apiKeyInputRef}
|
||||||
style={[styles.input, isInputFocused && styles.inputFocused]}
|
style={[styles.input, isInputFocused && styles.inputFocused]}
|
||||||
|
|
@ -298,7 +524,7 @@ const TMDBSettingsScreen = () => {
|
||||||
if (testResult) setTestResult(null);
|
if (testResult) setTestResult(null);
|
||||||
}}
|
}}
|
||||||
placeholder="Paste your TMDb API key (v4 auth)"
|
placeholder="Paste your TMDb API key (v4 auth)"
|
||||||
placeholderTextColor={colors.mediumGray}
|
placeholderTextColor={currentTheme.colors.mediumGray}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
|
@ -309,7 +535,7 @@ const TMDBSettingsScreen = () => {
|
||||||
style={styles.pasteButton}
|
style={styles.pasteButton}
|
||||||
onPress={pasteFromClipboard}
|
onPress={pasteFromClipboard}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="content-paste" size={20} color={colors.primary} />
|
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -339,7 +565,7 @@ const TMDBSettingsScreen = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={testResult.success ? "check-circle" : "error"}
|
name={testResult.success ? "check-circle" : "error"}
|
||||||
size={18}
|
size={18}
|
||||||
color={testResult.success ? colors.success : colors.error}
|
color={testResult.success ? currentTheme.colors.success : currentTheme.colors.error}
|
||||||
style={styles.resultIcon}
|
style={styles.resultIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
|
|
@ -355,7 +581,7 @@ const TMDBSettingsScreen = () => {
|
||||||
style={styles.helpLink}
|
style={styles.helpLink}
|
||||||
onPress={openTMDBWebsite}
|
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}>
|
<Text style={styles.helpText}>
|
||||||
How to get a TMDb API key?
|
How to get a TMDb API key?
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -363,7 +589,7 @@ const TMDBSettingsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.infoCard}>
|
<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}>
|
<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.
|
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.
|
Using your own API key gives you dedicated quota and may improve app performance.
|
||||||
|
|
@ -374,7 +600,7 @@ const TMDBSettingsScreen = () => {
|
||||||
|
|
||||||
{!useCustomKey && (
|
{!useCustomKey && (
|
||||||
<View style={styles.infoCard}>
|
<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}>
|
<Text style={styles.infoText}>
|
||||||
Currently using the built-in TMDb API key. This key is shared among all users.
|
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.
|
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;
|
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 { makeRedirectUri, useAuthRequest, ResponseType, Prompt, CodeChallengeMethod } from 'expo-auth-session';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { traktService, TraktUser } from '../services/traktService';
|
import { traktService, TraktUser } from '../services/traktService';
|
||||||
import { colors } from '../styles/colors';
|
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
|
|
@ -43,6 +43,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
const checkAuthStatus = useCallback(async () => {
|
const checkAuthStatus = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -151,7 +152,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[
|
<SafeAreaView style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
|
{ backgroundColor: isDarkMode ? currentTheme.colors.darkBackground : '#F2F2F7' }
|
||||||
]}>
|
]}>
|
||||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
|
|
@ -162,12 +163,12 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="arrow-back"
|
name="arrow-back"
|
||||||
size={24}
|
size={24}
|
||||||
color={isDarkMode ? colors.highEmphasis : colors.textDark}
|
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.headerTitle,
|
styles.headerTitle,
|
||||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
]}>
|
]}>
|
||||||
Trakt Settings
|
Trakt Settings
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -179,11 +180,11 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.card,
|
styles.card,
|
||||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
|
||||||
]}>
|
]}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
</View>
|
</View>
|
||||||
) : isAuthenticated && userProfile ? (
|
) : isAuthenticated && userProfile ? (
|
||||||
<View style={styles.profileContainer}>
|
<View style={styles.profileContainer}>
|
||||||
|
|
@ -194,7 +195,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.avatarPlaceholder, { backgroundColor: colors.primary }]}>
|
<View style={[styles.avatarPlaceholder, { backgroundColor: currentTheme.colors.primary }]}>
|
||||||
<Text style={styles.avatarText}>
|
<Text style={styles.avatarText}>
|
||||||
{userProfile.name?.charAt(0) || userProfile.username.charAt(0)}
|
{userProfile.name?.charAt(0) || userProfile.username.charAt(0)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -203,13 +204,13 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
<View style={styles.profileInfo}>
|
<View style={styles.profileInfo}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.profileName,
|
styles.profileName,
|
||||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
]}>
|
]}>
|
||||||
{userProfile.name || userProfile.username}
|
{userProfile.name || userProfile.username}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.profileUsername,
|
styles.profileUsername,
|
||||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||||
]}>
|
]}>
|
||||||
@{userProfile.username}
|
@{userProfile.username}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -224,7 +225,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
<View style={styles.statsContainer}>
|
<View style={styles.statsContainer}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.joinedDate,
|
styles.joinedDate,
|
||||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||||
]}>
|
]}>
|
||||||
Joined {new Date(userProfile.joined_at).toLocaleDateString()}
|
Joined {new Date(userProfile.joined_at).toLocaleDateString()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -252,20 +253,20 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.signInTitle,
|
styles.signInTitle,
|
||||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
]}>
|
]}>
|
||||||
Connect with Trakt
|
Connect with Trakt
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.signInDescription,
|
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
|
Sync your watch history, watchlist, and collection with Trakt.tv
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
{ backgroundColor: isDarkMode ? colors.primary : colors.primary }
|
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
|
||||||
]}
|
]}
|
||||||
onPress={handleSignIn}
|
onPress={handleSignIn}
|
||||||
disabled={!request || isExchangingCode} // Disable while waiting for response or exchanging code
|
disabled={!request || isExchangingCode} // Disable while waiting for response or exchanging code
|
||||||
|
|
@ -285,25 +286,25 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.card,
|
styles.card,
|
||||||
{ backgroundColor: isDarkMode ? colors.elevation2 : colors.white }
|
{ backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white }
|
||||||
]}>
|
]}>
|
||||||
<View style={styles.settingsSection}>
|
<View style={styles.settingsSection}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.sectionTitle,
|
styles.sectionTitle,
|
||||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
]}>
|
]}>
|
||||||
Sync Settings
|
Sync Settings
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.settingItem}>
|
<View style={styles.settingItem}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.settingLabel,
|
styles.settingLabel,
|
||||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
]}>
|
]}>
|
||||||
Auto-sync playback progress
|
Auto-sync playback progress
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.settingDescription,
|
styles.settingDescription,
|
||||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||||
]}>
|
]}>
|
||||||
Coming soon
|
Coming soon
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -311,13 +312,13 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
<View style={styles.settingItem}>
|
<View style={styles.settingItem}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.settingLabel,
|
styles.settingLabel,
|
||||||
{ color: isDarkMode ? colors.highEmphasis : colors.textDark }
|
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }
|
||||||
]}>
|
]}>
|
||||||
Import watched history
|
Import watched history
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.settingDescription,
|
styles.settingDescription,
|
||||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||||
]}>
|
]}>
|
||||||
Coming soon
|
Coming soon
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -331,7 +332,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.buttonText,
|
styles.buttonText,
|
||||||
{ color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }
|
{ color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }
|
||||||
]}>
|
]}>
|
||||||
Sync Now (Coming Soon)
|
Sync Now (Coming Soon)
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,40 @@
|
||||||
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
import { StyleSheet, Dimensions, Platform } from 'react-native';
|
||||||
import { colors } from './colors';
|
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
export const POSTER_WIDTH = (width - 50) / 3;
|
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: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
|
||||||
},
|
},
|
||||||
scrollContent: {
|
section: {
|
||||||
paddingBottom: 40,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
loadingMainContainer: {
|
sectionHeader: {
|
||||||
flex: 1,
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingBottom: 40,
|
marginBottom: 12,
|
||||||
|
paddingHorizontal: HORIZONTAL_PADDING,
|
||||||
},
|
},
|
||||||
loadingText: {
|
sectionTitle: {
|
||||||
color: colors.textMuted,
|
fontSize: 18,
|
||||||
marginTop: 12,
|
fontWeight: '700',
|
||||||
fontSize: 14,
|
|
||||||
},
|
},
|
||||||
emptyCatalog: {
|
seeAllButton: {
|
||||||
padding: 32,
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.elevation1,
|
|
||||||
margin: 16,
|
|
||||||
borderRadius: 16,
|
|
||||||
},
|
|
||||||
addCatalogButton: {
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
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: {
|
seeAllText: {
|
||||||
color: colors.white,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
marginRight: 4,
|
||||||
marginLeft: 8,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default homeStyles;
|
export default {
|
||||||
|
POSTER_WIDTH,
|
||||||
|
POSTER_HEIGHT,
|
||||||
|
HORIZONTAL_PADDING,
|
||||||
|
};
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
import { StyleSheet, Dimensions } from 'react-native';
|
import { StyleSheet, Dimensions } from 'react-native';
|
||||||
import { colors } from '../index';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
|
||||||
const useDiscoverStyles = () => {
|
const useDiscoverStyles = () => {
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
headerBackground: {
|
headerBackground: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
|
|
@ -36,7 +37,7 @@ const useDiscoverStyles = () => {
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: '800',
|
fontWeight: '800',
|
||||||
color: colors.white,
|
color: currentTheme.colors.white,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
},
|
},
|
||||||
searchButton: {
|
searchButton: {
|
||||||
|
|
@ -56,7 +57,7 @@ const useDiscoverStyles = () => {
|
||||||
paddingTop: 80,
|
paddingTop: 80,
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
color: colors.mediumGray,
|
color: currentTheme.colors.mediumGray,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingHorizontal: 32,
|
paddingHorizontal: 32,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue