Ios #14
5 changed files with 728 additions and 353 deletions
|
|
@ -2,7 +2,9 @@ import React, { useEffect, useState, useRef } from 'react';
|
|||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { Episode } from '../../types/metadata';
|
||||
import { tmdbService } from '../../services/tmdbService';
|
||||
import { storageService } from '../../services/storageService';
|
||||
|
|
@ -34,6 +36,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
metadata
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { width } = useWindowDimensions();
|
||||
const isTablet = width > 768;
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
|
|
@ -159,6 +162,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.seasonButtonText,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
selectedSeason === season && [styles.selectedSeasonButtonText, { color: currentTheme.colors.primary }]
|
||||
]}
|
||||
|
|
@ -173,7 +177,8 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const renderEpisodeCard = (episode: Episode) => {
|
||||
// Vertical layout episode card (traditional)
|
||||
const renderVerticalEpisodeCard = (episode: Episode) => {
|
||||
let episodeImage = EPISODE_PLACEHOLDER;
|
||||
if (episode.still_path) {
|
||||
const tmdbUrl = tmdbService.getImageUrl(episode.still_path, 'w500');
|
||||
|
|
@ -217,9 +222,9 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
<TouchableOpacity
|
||||
key={episode.id}
|
||||
style={[
|
||||
styles.episodeCard,
|
||||
isTablet && styles.episodeCardTablet,
|
||||
{ backgroundColor: currentTheme.colors.darkBackground }
|
||||
styles.episodeCardVertical,
|
||||
isTablet && styles.episodeCardVerticalTablet,
|
||||
{ backgroundColor: currentTheme.colors.elevation2 }
|
||||
]}
|
||||
onPress={() => onSelectEpisode(episode)}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -291,6 +296,127 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// Horizontal layout episode card (Netflix-style)
|
||||
const renderHorizontalEpisodeCard = (episode: Episode) => {
|
||||
let episodeImage = EPISODE_PLACEHOLDER;
|
||||
if (episode.still_path) {
|
||||
const tmdbUrl = tmdbService.getImageUrl(episode.still_path, 'w500');
|
||||
if (tmdbUrl) episodeImage = tmdbUrl;
|
||||
} else if (metadata?.poster) {
|
||||
episodeImage = metadata.poster;
|
||||
}
|
||||
|
||||
const episodeNumber = typeof episode.episode_number === 'number' ? episode.episode_number.toString() : '';
|
||||
const seasonNumber = typeof episode.season_number === 'number' ? episode.season_number.toString() : '';
|
||||
const episodeString = seasonNumber && episodeNumber ? `EPISODE ${episodeNumber}` : '';
|
||||
|
||||
const formatRuntime = (runtime: number) => {
|
||||
if (!runtime) return null;
|
||||
const hours = Math.floor(runtime / 60);
|
||||
const minutes = runtime % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
// Get episode progress
|
||||
const episodeId = episode.stremioId || `${metadata?.id}:${episode.season_number}:${episode.episode_number}`;
|
||||
const progress = episodeProgress[episodeId];
|
||||
const progressPercent = progress ? (progress.currentTime / progress.duration) * 100 : 0;
|
||||
|
||||
// Don't show progress bar if episode is complete (>= 95%)
|
||||
const showProgress = progress && progressPercent < 95;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={episode.id}
|
||||
style={[
|
||||
styles.episodeCardHorizontal,
|
||||
isTablet && styles.episodeCardHorizontalTablet
|
||||
]}
|
||||
onPress={() => onSelectEpisode(episode)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<Image
|
||||
source={{ uri: episodeImage }}
|
||||
style={styles.episodeBackgroundImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
|
||||
{/* Gradient Overlay */}
|
||||
<LinearGradient
|
||||
colors={[
|
||||
'rgba(0,0,0,0.1)',
|
||||
'rgba(0,0,0,0.3)',
|
||||
'rgba(0,0,0,0.8)',
|
||||
'rgba(0,0,0,0.95)'
|
||||
]}
|
||||
locations={[0, 0.3, 0.7, 1]}
|
||||
style={styles.episodeGradient}
|
||||
>
|
||||
{/* Content Container */}
|
||||
<View style={styles.episodeContent}>
|
||||
{/* Episode Number Badge */}
|
||||
<Text style={styles.episodeNumberHorizontal}>{episodeString}</Text>
|
||||
|
||||
{/* Episode Title */}
|
||||
<Text style={styles.episodeTitleHorizontal} numberOfLines={2}>
|
||||
{episode.name}
|
||||
</Text>
|
||||
|
||||
{/* Episode Description */}
|
||||
<Text style={styles.episodeDescriptionHorizontal} numberOfLines={3}>
|
||||
{episode.overview || 'No description available'}
|
||||
</Text>
|
||||
|
||||
{/* Metadata Row */}
|
||||
<View style={styles.episodeMetadataRowHorizontal}>
|
||||
{episode.runtime && (
|
||||
<Text style={styles.runtimeTextHorizontal}>
|
||||
{formatRuntime(episode.runtime)}
|
||||
</Text>
|
||||
)}
|
||||
{episode.vote_average > 0 && (
|
||||
<View style={styles.ratingContainerHorizontal}>
|
||||
<MaterialIcons name="star" size={14} color="#FFD700" />
|
||||
<Text style={styles.ratingTextHorizontal}>
|
||||
{episode.vote_average.toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{showProgress && (
|
||||
<View style={styles.progressBarContainerHorizontal}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressBarHorizontal,
|
||||
{ width: `${progressPercent}%`, backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Completed Badge */}
|
||||
{progressPercent >= 95 && (
|
||||
<View style={[styles.completedBadgeHorizontal, { backgroundColor: currentTheme.colors.primary }]}>
|
||||
<MaterialIcons name="check" size={16} color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* More Options */}
|
||||
<TouchableOpacity style={styles.moreButton} activeOpacity={0.7}>
|
||||
<MaterialIcons name="more-horiz" size={24} color="rgba(255,255,255,0.8)" />
|
||||
</TouchableOpacity>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || [];
|
||||
|
||||
return (
|
||||
|
|
@ -308,35 +434,62 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
{episodes.length} {episodes.length === 1 ? 'Episode' : 'Episodes'}
|
||||
</Text>
|
||||
|
||||
<ScrollView
|
||||
style={styles.episodeList}
|
||||
contentContainerStyle={[
|
||||
styles.episodeListContent,
|
||||
isTablet && styles.episodeListContentTablet
|
||||
]}
|
||||
>
|
||||
{isTablet ? (
|
||||
<View style={styles.episodeGrid}>
|
||||
{currentSeasonEpisodes.map((episode, index) => (
|
||||
{settings.episodeLayoutStyle === 'horizontal' ? (
|
||||
// Horizontal Layout (Netflix-style)
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.episodeList}
|
||||
contentContainerStyle={styles.episodeListContentHorizontal}
|
||||
decelerationRate="fast"
|
||||
snapToInterval={isTablet ? width * 0.4 + 16 : width * 0.85 + 16}
|
||||
snapToAlignment="start"
|
||||
>
|
||||
{currentSeasonEpisodes.map((episode, index) => (
|
||||
<Animated.View
|
||||
key={episode.id}
|
||||
entering={FadeIn.duration(400).delay(300 + index * 50)}
|
||||
style={[
|
||||
styles.episodeCardWrapperHorizontal,
|
||||
isTablet && styles.episodeCardWrapperHorizontalTablet
|
||||
]}
|
||||
>
|
||||
{renderHorizontalEpisodeCard(episode)}
|
||||
</Animated.View>
|
||||
))}
|
||||
</ScrollView>
|
||||
) : (
|
||||
// Vertical Layout (Traditional)
|
||||
<ScrollView
|
||||
style={styles.episodeList}
|
||||
contentContainerStyle={[
|
||||
styles.episodeListContentVertical,
|
||||
isTablet && styles.episodeListContentVerticalTablet
|
||||
]}
|
||||
>
|
||||
{isTablet ? (
|
||||
<View style={styles.episodeGridVertical}>
|
||||
{currentSeasonEpisodes.map((episode, index) => (
|
||||
<Animated.View
|
||||
key={episode.id}
|
||||
entering={FadeIn.duration(400).delay(300 + index * 50)}
|
||||
>
|
||||
{renderVerticalEpisodeCard(episode)}
|
||||
</Animated.View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
currentSeasonEpisodes.map((episode, index) => (
|
||||
<Animated.View
|
||||
key={episode.id}
|
||||
entering={FadeIn.duration(400).delay(300 + index * 50)}
|
||||
>
|
||||
{renderEpisodeCard(episode)}
|
||||
{renderVerticalEpisodeCard(episode)}
|
||||
</Animated.View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
currentSeasonEpisodes.map((episode, index) => (
|
||||
<Animated.View
|
||||
key={episode.id}
|
||||
entering={FadeIn.duration(400).delay(300 + index * 50)}
|
||||
>
|
||||
{renderEpisodeCard(episode)}
|
||||
</Animated.View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -366,18 +519,20 @@ const styles = StyleSheet.create({
|
|||
episodeList: {
|
||||
flex: 1,
|
||||
},
|
||||
episodeListContent: {
|
||||
|
||||
// Vertical Layout Styles
|
||||
episodeListContentVertical: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
episodeListContentTablet: {
|
||||
episodeListContentVerticalTablet: {
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
episodeGrid: {
|
||||
episodeGridVertical: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
episodeCard: {
|
||||
episodeCardVertical: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
|
|
@ -385,13 +540,13 @@ const styles = StyleSheet.create({
|
|||
elevation: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 12,
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.05)',
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
height: 120,
|
||||
},
|
||||
episodeCardTablet: {
|
||||
episodeCardVerticalTablet: {
|
||||
width: '48%',
|
||||
flexDirection: 'column',
|
||||
height: 120,
|
||||
|
|
@ -410,12 +565,12 @@ const styles = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
bottom: 8,
|
||||
right: 4,
|
||||
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
borderColor: 'rgba(255,255,255,0.2)',
|
||||
zIndex: 1,
|
||||
},
|
||||
episodeNumberText: {
|
||||
|
|
@ -446,7 +601,7 @@ const styles = StyleSheet.create({
|
|||
ratingContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
|
|
@ -461,6 +616,19 @@ const styles = StyleSheet.create({
|
|||
fontWeight: '700',
|
||||
marginLeft: 4,
|
||||
},
|
||||
runtimeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
runtimeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
},
|
||||
airDateText: {
|
||||
fontSize: 12,
|
||||
opacity: 0.8,
|
||||
|
|
@ -469,6 +637,161 @@ const styles = StyleSheet.create({
|
|||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
progressBarContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
progressBar: {
|
||||
height: '100%',
|
||||
},
|
||||
completedBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
},
|
||||
|
||||
// Horizontal Layout Styles
|
||||
episodeListContentHorizontal: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 16,
|
||||
},
|
||||
episodeCardWrapperHorizontal: {
|
||||
width: Dimensions.get('window').width * 0.85,
|
||||
marginRight: 16,
|
||||
},
|
||||
episodeCardWrapperHorizontalTablet: {
|
||||
width: Dimensions.get('window').width * 0.4,
|
||||
},
|
||||
episodeCardHorizontal: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
elevation: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.05)',
|
||||
height: 200,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
},
|
||||
episodeCardHorizontalTablet: {
|
||||
height: 180,
|
||||
},
|
||||
episodeBackgroundImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 16,
|
||||
},
|
||||
episodeGradient: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 16,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
episodeContent: {
|
||||
padding: 16,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
episodeNumberHorizontal: {
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 4,
|
||||
},
|
||||
episodeTitleHorizontal: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.3,
|
||||
marginBottom: 8,
|
||||
lineHeight: 22,
|
||||
},
|
||||
episodeDescriptionHorizontal: {
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
marginBottom: 12,
|
||||
opacity: 0.9,
|
||||
},
|
||||
episodeMetadataRowHorizontal: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
runtimeTextHorizontal: {
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
ratingContainerHorizontal: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 4,
|
||||
gap: 3,
|
||||
},
|
||||
ratingTextHorizontal: {
|
||||
color: '#FFD700',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
progressBarContainerHorizontal: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
},
|
||||
progressBarHorizontal: {
|
||||
height: '100%',
|
||||
borderRadius: 2,
|
||||
},
|
||||
completedBadgeHorizontal: {
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
moreButton: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
},
|
||||
|
||||
// Season Selector Styles
|
||||
seasonSelectorWrapper: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
|
@ -517,54 +840,4 @@ const styles = StyleSheet.create({
|
|||
selectedSeasonButtonText: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
progressBarContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
progressBar: {
|
||||
height: '100%',
|
||||
},
|
||||
progressTextContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
},
|
||||
progressText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
},
|
||||
completedBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.3)',
|
||||
},
|
||||
runtimeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
runtimeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
|
|
@ -35,6 +35,7 @@ export interface AppSettings {
|
|||
logoSourcePreference: 'metahub' | 'tmdb'; // Preferred source for title logos
|
||||
tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code)
|
||||
enableInternalProviders: boolean; // Toggle for internal providers like HDRezka
|
||||
episodeLayoutStyle: 'vertical' | 'horizontal'; // Layout style for episode cards
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
|
|
@ -52,6 +53,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
logoSourcePreference: 'metahub', // Default to Metahub as first source
|
||||
tmdbLanguagePreference: 'en', // Default to English
|
||||
enableInternalProviders: true, // Enable internal providers by default
|
||||
episodeLayoutStyle: 'horizontal', // Default to the new horizontal layout
|
||||
};
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ import { NavigationProp } from '@react-navigation/native';
|
|||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
|
||||
import Constants, { ExecutionEnvironment } from 'expo-constants';
|
||||
import axios from 'axios';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
|
|
@ -552,6 +554,36 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
blurOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
},
|
||||
androidBlurContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
androidBlur: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
androidFallbackBlur: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
});
|
||||
|
||||
const AddonsScreen = () => {
|
||||
|
|
@ -1233,7 +1265,24 @@ const AddonsScreen = () => {
|
|||
setAddonDetails(null);
|
||||
}}
|
||||
>
|
||||
<BlurView intensity={80} style={styles.modalContainer} tint="dark">
|
||||
<View style={styles.modalContainer}>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<ExpoBlurView intensity={80} style={styles.blurOverlay} tint="dark" />
|
||||
) : (
|
||||
Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
|
||||
<View style={[styles.androidBlurContainer, styles.androidFallbackBlur]} />
|
||||
) : (
|
||||
<View style={styles.androidBlurContainer}>
|
||||
<CommunityBlurView
|
||||
style={styles.androidBlur}
|
||||
blurType="dark"
|
||||
blurAmount={8}
|
||||
overlayColor="rgba(0,0,0,0.4)"
|
||||
reducedTransparencyFallbackColor="black"
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
<View style={styles.modalContent}>
|
||||
{addonDetails && (
|
||||
<>
|
||||
|
|
@ -1332,7 +1381,7 @@ const AddonsScreen = () => {
|
|||
</>
|
||||
)}
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -367,6 +367,51 @@ const SettingsScreen: React.FC = () => {
|
|||
icon="palette"
|
||||
renderControl={ChevronRight}
|
||||
onPress={() => navigation.navigate('ThemeSettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title="Episode Layout"
|
||||
description={settings.episodeLayoutStyle === 'horizontal' ? 'Horizontal Cards' : 'Vertical List'}
|
||||
icon="view-module"
|
||||
renderControl={() => (
|
||||
<View style={styles.selectorContainer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
settings.episodeLayoutStyle === 'vertical' && {
|
||||
backgroundColor: currentTheme.colors.primary
|
||||
}
|
||||
]}
|
||||
onPress={() => updateSetting('episodeLayoutStyle', 'vertical')}
|
||||
>
|
||||
<Text style={[
|
||||
styles.selectorText,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
settings.episodeLayoutStyle === 'vertical' && {
|
||||
color: currentTheme.colors.white,
|
||||
fontWeight: '600'
|
||||
}
|
||||
]}>Vertical</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.selectorButton,
|
||||
settings.episodeLayoutStyle === 'horizontal' && {
|
||||
backgroundColor: currentTheme.colors.primary
|
||||
}
|
||||
]}
|
||||
onPress={() => updateSetting('episodeLayoutStyle', 'horizontal')}
|
||||
>
|
||||
<Text style={[
|
||||
styles.selectorText,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
settings.episodeLayoutStyle === 'horizontal' && {
|
||||
color: currentTheme.colors.white,
|
||||
fontWeight: '600'
|
||||
}
|
||||
]}>Horizontal</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
isLast={true}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ import { tmdbService } from '../services/tmdbService';
|
|||
import { useSettings } from '../hooks/useSettings';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
const TMDBSettingsScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
|
|
@ -41,6 +41,7 @@ const TMDBSettingsScreen = () => {
|
|||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const apiKeyInputRef = useRef<TextInput>(null);
|
||||
const { currentTheme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
logger.log('[TMDBSettingsScreen] Component mounted');
|
||||
|
|
@ -222,277 +223,66 @@ 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,
|
||||
},
|
||||
});
|
||||
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
||||
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
||||
const headerHeight = headerBaseHeight + topSpacing;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={styles.loadingText}>Loading Settings...</Text>
|
||||
<Text style={[styles.loadingText, { color: currentTheme.colors.text }]}>Loading Settings...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||
TMDb Settings
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.title}>TMDb Settings</Text>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.switchCard}>
|
||||
<View style={[styles.switchCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.switchTextContainer}>
|
||||
<Text style={styles.switchTitle}>Use Custom TMDb API Key</Text>
|
||||
<Text style={[styles.switchTitle, { color: currentTheme.colors.text }]}>Use Custom TMDb API Key</Text>
|
||||
<Text style={[styles.switchDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
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>
|
||||
</View>
|
||||
<Switch
|
||||
value={useCustomKey}
|
||||
onValueChange={toggleUseCustomKey}
|
||||
trackColor={{ false: currentTheme.colors.lightGray, true: currentTheme.colors.accentLight }}
|
||||
thumbColor={Platform.OS === 'android' ? currentTheme.colors.primary : ''}
|
||||
ios_backgroundColor={currentTheme.colors.lightGray}
|
||||
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? (useCustomKey ? currentTheme.colors.white : currentTheme.colors.white) : ''}
|
||||
ios_backgroundColor={'rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.switchDescription}>
|
||||
Enable to use your own TMDb API key instead of the built-in one.
|
||||
Using your own API key may provide better performance and higher rate limits.
|
||||
</Text>
|
||||
|
||||
{useCustomKey && (
|
||||
<>
|
||||
<View style={styles.statusCard}>
|
||||
<View style={[styles.statusCard, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<MaterialIcons
|
||||
name={isKeySet ? "check-circle" : "error-outline"}
|
||||
size={28}
|
||||
|
|
@ -500,10 +290,10 @@ const TMDBSettingsScreen = () => {
|
|||
style={styles.statusIconContainer}
|
||||
/>
|
||||
<View style={styles.statusTextContainer}>
|
||||
<Text style={styles.statusTitle}>
|
||||
<Text style={[styles.statusTitle, { color: currentTheme.colors.text }]}>
|
||||
{isKeySet ? "API Key Active" : "API Key Required"}
|
||||
</Text>
|
||||
<Text style={styles.statusDescription}>
|
||||
<Text style={[styles.statusDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{isKeySet
|
||||
? "Your custom TMDb API key is set and active."
|
||||
: "Add your TMDb API key below."}
|
||||
|
|
@ -511,19 +301,26 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>API Key</Text>
|
||||
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.text }]}>API Key</Text>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
ref={apiKeyInputRef}
|
||||
style={[styles.input, isInputFocused && styles.inputFocused]}
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.elevation1,
|
||||
color: currentTheme.colors.text,
|
||||
borderColor: isInputFocused ? currentTheme.colors.primary : 'transparent'
|
||||
}
|
||||
]}
|
||||
value={apiKey}
|
||||
onChangeText={(text) => {
|
||||
setApiKey(text);
|
||||
if (testResult) setTestResult(null);
|
||||
}}
|
||||
placeholder="Paste your TMDb API key (v3)"
|
||||
placeholderTextColor={currentTheme.colors.mediumGray}
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
spellCheck={false}
|
||||
|
|
@ -540,18 +337,18 @@ const TMDBSettingsScreen = () => {
|
|||
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={saveApiKey}
|
||||
>
|
||||
<Text style={styles.buttonText}>Save API Key</Text>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save API Key</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{isKeySet && (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.clearButton]}
|
||||
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
|
||||
onPress={clearApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, styles.clearButtonText]}>Clear</Text>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -559,7 +356,7 @@ const TMDBSettingsScreen = () => {
|
|||
{testResult && (
|
||||
<View style={[
|
||||
styles.resultMessage,
|
||||
testResult.success ? styles.successMessage : styles.errorMessage
|
||||
{ backgroundColor: testResult.success ? currentTheme.colors.success + '1A' : currentTheme.colors.error + '1A' }
|
||||
]}>
|
||||
<MaterialIcons
|
||||
name={testResult.success ? "check-circle" : "error"}
|
||||
|
|
@ -569,7 +366,7 @@ const TMDBSettingsScreen = () => {
|
|||
/>
|
||||
<Text style={[
|
||||
styles.resultText,
|
||||
testResult.success ? styles.successText : styles.errorText
|
||||
{ color: testResult.success ? currentTheme.colors.success : currentTheme.colors.error }
|
||||
]}>
|
||||
{testResult.message}
|
||||
</Text>
|
||||
|
|
@ -581,15 +378,15 @@ const TMDBSettingsScreen = () => {
|
|||
onPress={openTMDBWebsite}
|
||||
>
|
||||
<MaterialIcons name="help" size={16} color={currentTheme.colors.primary} style={styles.helpIcon} />
|
||||
<Text style={styles.helpText}>
|
||||
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
|
||||
How to get a TMDb API key?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoCard}>
|
||||
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
|
||||
<Text style={styles.infoText}>
|
||||
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
To get your own TMDb API key (v3), 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.
|
||||
</Text>
|
||||
|
|
@ -598,17 +395,226 @@ const TMDBSettingsScreen = () => {
|
|||
)}
|
||||
|
||||
{!useCustomKey && (
|
||||
<View style={styles.infoCard}>
|
||||
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="info-outline" size={22} color={currentTheme.colors.primary} style={styles.infoIcon} />
|
||||
<Text style={styles.infoText}>
|
||||
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
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.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
headerContainer: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 8,
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 2,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
backButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginLeft: 4,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 32,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.3,
|
||||
paddingLeft: 4,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
switchCard: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
switchTextContainer: {
|
||||
flex: 1,
|
||||
marginRight: 16,
|
||||
},
|
||||
switchTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
switchDescription: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
opacity: 0.8,
|
||||
},
|
||||
statusCard: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
statusIconContainer: {
|
||||
marginRight: 12,
|
||||
},
|
||||
statusTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
statusTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
statusDescription: {
|
||||
fontSize: 14,
|
||||
opacity: 0.8,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
fontSize: 15,
|
||||
borderWidth: 2,
|
||||
},
|
||||
pasteButton: {
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
padding: 8,
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
button: {
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
},
|
||||
clearButton: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
marginRight: 0,
|
||||
marginLeft: 8,
|
||||
flex: 0,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
buttonText: {
|
||||
fontWeight: '600',
|
||||
fontSize: 15,
|
||||
},
|
||||
resultMessage: {
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
resultIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
resultText: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
helpLink: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
helpIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
helpText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
infoCard: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
infoIcon: {
|
||||
marginRight: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
opacity: 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
export default TMDBSettingsScreen;
|
||||
Loading…
Reference in a new issue