From 2e5de7216b2bbb5a8551bd10deb90a3b3b7bd6b5 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 18 Jun 2025 11:25:54 +0530 Subject: [PATCH] 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. --- src/components/metadata/SeriesContent.tsx | 451 ++++++++++++++---- src/hooks/useSettings.ts | 2 + src/screens/AddonsScreen.tsx | 55 ++- src/screens/SettingsScreen.tsx | 45 ++ src/screens/TMDBSettingsScreen.tsx | 528 +++++++++++----------- 5 files changed, 728 insertions(+), 353 deletions(-) diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index a35c403cf..8868b283f 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -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 = ({ 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 = ({ = ({ ); }; - 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 = ({ onSelectEpisode(episode)} activeOpacity={0.7} @@ -291,6 +296,127 @@ export const SeriesContent: React.FC = ({ ); }; + // 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 ( + onSelectEpisode(episode)} + activeOpacity={0.85} + > + {/* Background Image */} + + + {/* Gradient Overlay */} + + {/* Content Container */} + + {/* Episode Number Badge */} + {episodeString} + + {/* Episode Title */} + + {episode.name} + + + {/* Episode Description */} + + {episode.overview || 'No description available'} + + + {/* Metadata Row */} + + {episode.runtime && ( + + {formatRuntime(episode.runtime)} + + )} + {episode.vote_average > 0 && ( + + + + {episode.vote_average.toFixed(1)} + + + )} + + + + {/* Progress Bar */} + {showProgress && ( + + + + )} + + {/* Completed Badge */} + {progressPercent >= 95 && ( + + + + )} + + {/* More Options */} + + + + + + ); + }; + const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; return ( @@ -308,35 +434,62 @@ export const SeriesContent: React.FC = ({ {episodes.length} {episodes.length === 1 ? 'Episode' : 'Episodes'} - - {isTablet ? ( - - {currentSeasonEpisodes.map((episode, index) => ( + {settings.episodeLayoutStyle === 'horizontal' ? ( + // Horizontal Layout (Netflix-style) + + {currentSeasonEpisodes.map((episode, index) => ( + + {renderHorizontalEpisodeCard(episode)} + + ))} + + ) : ( + // Vertical Layout (Traditional) + + {isTablet ? ( + + {currentSeasonEpisodes.map((episode, index) => ( + + {renderVerticalEpisodeCard(episode)} + + ))} + + ) : ( + currentSeasonEpisodes.map((episode, index) => ( - {renderEpisodeCard(episode)} + {renderVerticalEpisodeCard(episode)} - ))} - - ) : ( - currentSeasonEpisodes.map((episode, index) => ( - - {renderEpisodeCard(episode)} - - )) - )} - + )) + )} + + )} ); @@ -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, - }, }); \ No newline at end of file diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index aa63dce10..bb1354683 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -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'; diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx index 1761a9a5d..c9b39bb57 100644 --- a/src/screens/AddonsScreen.tsx +++ b/src/screens/AddonsScreen.tsx @@ -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); }} > - + + {Platform.OS === 'ios' ? ( + + ) : ( + Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? ( + + ) : ( + + + + ) + )} {addonDetails && ( <> @@ -1332,7 +1381,7 @@ const AddonsScreen = () => { )} - + ); diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index bc7471cad..9b1dcf166 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -367,6 +367,51 @@ const SettingsScreen: React.FC = () => { icon="palette" renderControl={ChevronRight} onPress={() => navigation.navigate('ThemeSettings')} + /> + ( + + updateSetting('episodeLayoutStyle', 'vertical')} + > + Vertical + + updateSetting('episodeLayoutStyle', 'horizontal')} + > + Horizontal + + + )} isLast={true} /> diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index f65540552..660ecd39e 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -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(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 ( - + - Loading Settings... + Loading Settings... - + ); } return ( - + - - navigation.goBack()} - > - - Settings - + + + navigation.goBack()} + > + + Settings + + + + TMDb Settings + - TMDb Settings - + - Use Custom TMDb API Key + Use Custom TMDb API Key + + Enable to use your own TMDb API key instead of the built-in one. + Using your own API key may provide better performance and higher rate limits. + - - Enable to use your own TMDb API key instead of the built-in one. - Using your own API key may provide better performance and higher rate limits. - - {useCustomKey && ( <> - + { style={styles.statusIconContainer} /> - + {isKeySet ? "API Key Active" : "API Key Required"} - + {isKeySet ? "Your custom TMDb API key is set and active." : "Add your TMDb API key below."} @@ -511,19 +301,26 @@ const TMDBSettingsScreen = () => { - - API Key + + API Key { 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 = () => { - Save API Key + Save API Key {isKeySet && ( - Clear + Clear )} @@ -559,7 +356,7 @@ const TMDBSettingsScreen = () => { {testResult && ( { /> {testResult.message} @@ -581,15 +378,15 @@ const TMDBSettingsScreen = () => { onPress={openTMDBWebsite} > - + How to get a TMDb API key? - + - + 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. @@ -598,17 +395,226 @@ const TMDBSettingsScreen = () => { )} {!useCustomKey && ( - + - + Currently using the built-in TMDb API key. This key is shared among all users. For better performance and reliability, consider using your own API key. )} - + ); }; +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; \ No newline at end of file