mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Implement episode layout settings and enhance SeriesContent display
This update introduces a new episode layout setting, allowing users to choose between vertical and horizontal card styles for episode displays. The SeriesContent component has been refactored to support both layouts, improving the user experience with a more dynamic presentation of episodes. Additionally, new styles and components have been added for the horizontal layout, including gradient overlays and progress indicators, enhancing visual appeal and functionality. The settings screen has also been updated to allow users to toggle between layout styles seamlessly.
This commit is contained in:
parent
81897b7242
commit
2e5de7216b
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