diff --git a/src/components/CustomAlert.tsx b/src/components/CustomAlert.tsx index 0c72bdd..574ff49 100644 --- a/src/components/CustomAlert.tsx +++ b/src/components/CustomAlert.tsx @@ -44,13 +44,13 @@ export const CustomAlert = ({ const themeColors = currentTheme.colors; useEffect(() => { - const animDuration = Platform.OS === 'android' ? 200 : 120; + const duration = Platform.OS === 'android' ? 200 : 150; if (visible) { - opacity.value = withTiming(1, { duration: animDuration }); - scale.value = withTiming(1, { duration: animDuration }); + opacity.value = withTiming(1, { duration }); + scale.value = withTiming(1, { duration }); } else { - opacity.value = withTiming(0, { duration: animDuration }); - scale.value = withTiming(0.95, { duration: animDuration }); + opacity.value = withTiming(0, { duration }); + scale.value = withTiming(0.95, { duration }); } }, [visible]); @@ -63,10 +63,6 @@ export const CustomAlert = ({ opacity: opacity.value, })); - const backgroundColor = isDarkMode ? themeColors.darkBackground : themeColors.elevation2 || '#FFFFFF'; - const textColor = isDarkMode ? themeColors.white : themeColors.black || '#000000'; - const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; - // Safe action handler to prevent crashes const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => { try { @@ -95,23 +91,54 @@ export const CustomAlert = ({ statusBarTranslucent={false} hardwareAccelerated={true} > - + - - {title} - {message} + + {/* Title */} + + {title} + + + {/* Message */} + + {message} + + + {/* Actions */} - {actions.map((action, idx) => ( - handleActionPress(action)} - activeOpacity={0.7} - > - {action.label} - - ))} + {actions.map((action, idx) => { + const isPrimary = idx === actions.length - 1; + return ( + handleActionPress(action)} + activeOpacity={0.7} + > + + {action.label} + + + ); + })} @@ -129,23 +156,55 @@ export const CustomAlert = ({ onRequestClose={onClose} presentationStyle="overFullScreen" > - + - - {title} - {message} + + {/* Title */} + + {title} + + + {/* Message */} + + {message} + + + {/* Actions */} - {actions.map((action, idx) => ( - handleActionPress(action)} - activeOpacity={0.7} - > - {action.label} - - ))} + {actions.map((action, idx) => { + const isPrimary = idx === actions.length - 1; + return ( + handleActionPress(action)} + activeOpacity={0.7} + > + + {action.label} + + + ); + })} @@ -167,52 +226,66 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', + paddingHorizontal: 24, }, alertContainer: { - minWidth: 280, - maxWidth: '85%', - borderRadius: 20, - padding: 24, + width: '100%', + maxWidth: 340, + borderRadius: 24, + padding: 28, borderWidth: 1, + borderColor: '#007AFF', // iOS blue - will be overridden by theme + overflow: 'hidden', // Ensure background fills entire card ...Platform.select({ ios: { shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.15, - shadowRadius: 8, + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.3, + shadowRadius: 24, }, android: { - elevation: 8, + elevation: 12, }, }), }, title: { - fontSize: 18, + fontSize: 20, fontWeight: '700', - marginBottom: 12, + marginBottom: 8, textAlign: 'center', + letterSpacing: 0.2, }, message: { - fontSize: 16, - marginBottom: 20, + fontSize: 15, + marginBottom: 24, textAlign: 'center', + lineHeight: 22, + letterSpacing: 0.1, }, actionsRow: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12, + marginTop: 4, }, actionButton: { - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, + paddingHorizontal: 20, + paddingVertical: 11, + borderRadius: 12, + minWidth: 80, + alignItems: 'center', + justifyContent: 'center', }, - lastActionButton: { - // Optionally style the last button differently + primaryButton: { + // Background color set dynamically via theme + }, + secondaryButton: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', }, actionText: { fontSize: 16, fontWeight: '600', + letterSpacing: 0.2, }, }); diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index b13f7ea..139445e 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -38,7 +38,6 @@ import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; import TrailerService from '../../services/trailerService'; import TrailerPlayer from '../video/TrailerPlayer'; -import { isTmdbUrl } from '../../utils/logoUtils'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; @@ -895,17 +894,10 @@ const HeroSection: React.FC = memo(({ bannerImage || metadata.banner || metadata.poster , [bannerImage, metadata.banner, metadata.poster]); - // Prefer TMDB logo when enrichment is enabled; fallback to addon's logo + // Use the logo provided by metadata (already enriched by useMetadataAssets based on settings) const logoUri = useMemo(() => { - const candidate = metadata?.logo as string | undefined; - if (!candidate) return undefined; - if (settings?.enrichMetadataWithTMDB) { - // If the current logo is a TMDB URL, use it; otherwise still use available logo - if (isTmdbUrl(candidate)) return candidate; - return candidate; - } - return candidate; - }, [metadata.logo, settings?.enrichMetadataWithTMDB]); + return metadata?.logo as string | undefined; + }, [metadata?.logo]); // Performance optimization: Lazy loading setup useEffect(() => { diff --git a/src/screens/DownloadsScreen.tsx b/src/screens/DownloadsScreen.tsx index 122942f..7d5fe92 100644 --- a/src/screens/DownloadsScreen.tsx +++ b/src/screens/DownloadsScreen.tsx @@ -26,11 +26,34 @@ import Animated, { import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; import { LinearGradient } from 'expo-linear-gradient'; +import FastImage from '@d11/react-native-fast-image'; import { useDownloads } from '../contexts/DownloadsContext'; import type { DownloadItem } from '../contexts/DownloadsContext'; import { Toast } from 'toastify-react-native'; +import CustomAlert from '../components/CustomAlert'; const { height, width } = Dimensions.get('window'); +const isTablet = width >= 768; + +// Tablet-optimized poster sizes +const HORIZONTAL_ITEM_WIDTH = isTablet ? width * 0.18 : width * 0.3; +const HORIZONTAL_POSTER_HEIGHT = HORIZONTAL_ITEM_WIDTH * 1.5; +const POSTER_WIDTH = isTablet ? 70 : 90; +const POSTER_HEIGHT = isTablet ? 105 : 135; + +// Helper function to optimize poster URLs +const optimizePosterUrl = (poster: string | undefined | null): string => { + if (!poster || poster.includes('placeholder')) { + return 'https://via.placeholder.com/80x120/333333/666666?text=No+Image'; + } + + // For TMDB images, use larger sizes for bigger posters + if (poster.includes('image.tmdb.org')) { + return poster.replace(/\/w\d+\//, '/w300/'); + } + + return poster; +}; // Download items come from DownloadsContext @@ -74,6 +97,16 @@ const DownloadItemComponent: React.FC<{ onAction: (item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => void; }> = React.memo(({ item, onPress, onAction }) => { const { currentTheme } = useTheme(); + const [posterUrl, setPosterUrl] = useState(item.posterUrl || null); + + // Try to fetch poster if not available + useEffect(() => { + if (!posterUrl && (item.imdbId || item.tmdbId)) { + // This could be enhanced to fetch poster from TMDB API if needed + // For now, we'll use the existing posterUrl or fallback to placeholder + setPosterUrl(item.posterUrl || null); + } + }, [item.imdbId, item.tmdbId, item.posterUrl, posterUrl]); const handleLongPress = useCallback(() => { if (item.status === 'completed' && item.fileUri) { @@ -171,6 +204,29 @@ const DownloadItemComponent: React.FC<{ onLongPress={handleLongPress} activeOpacity={0.8} > + {/* Poster */} + + + {/* Status indicator overlay */} + + + + + {/* Content info */} @@ -208,7 +264,21 @@ const DownloadItemComponent: React.FC<{ {formatBytes(item.downloadedBytes)} / {item.totalBytes ? formatBytes(item.totalBytes) : '—'} - + + {/* Warning for small files */} + {item.totalBytes && item.totalBytes < 1048576 && ( // Less than 1MB + + + + May not play - streaming playlist + + + )} + {/* Progress bar */} { const [isRefreshing, setIsRefreshing] = useState(false); const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed' | 'paused'>('all'); + const [showHelpAlert, setShowHelpAlert] = useState(false); // Animation values const headerOpacity = useSharedValue(1); @@ -383,6 +454,11 @@ const DownloadsScreen: React.FC = () => { setSelectedFilter(filter); }, []); + const showDownloadHelp = useCallback(() => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + setShowHelpAlert(true); + }, []); + // Focus effect useFocusEffect( useCallback(() => { @@ -462,9 +538,22 @@ const DownloadsScreen: React.FC = () => { }, headerStyle, ]}> - - Downloads - + + + Downloads + + + + + {downloads.length > 0 && ( @@ -518,6 +607,14 @@ const DownloadsScreen: React.FC = () => { )} /> )} + + {/* Help Alert */} + setShowHelpAlert(false)} + /> ); }; @@ -527,29 +624,38 @@ const styles = StyleSheet.create({ flex: 1, }, header: { - paddingHorizontal: 20, - paddingBottom: 16, + paddingHorizontal: isTablet ? 24 : 20, + paddingBottom: isTablet ? 20 : 16, borderBottomWidth: StyleSheet.hairlineWidth, }, + headerTitleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: isTablet ? 20 : 16, + }, headerTitle: { - fontSize: 32, + fontSize: isTablet ? 36 : 32, fontWeight: '700', - marginBottom: 16, + }, + helpButton: { + padding: 8, + marginLeft: 8, }, filterContainer: { flexDirection: 'row', - gap: 12, + gap: isTablet ? 16 : 12, }, filterButton: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 8, + paddingHorizontal: isTablet ? 20 : 16, + paddingVertical: isTablet ? 10 : 8, borderRadius: 20, gap: 8, }, filterButtonText: { - fontSize: 14, + fontSize: isTablet ? 16 : 14, fontWeight: '600', }, filterBadge: { @@ -565,24 +671,54 @@ const styles = StyleSheet.create({ fontWeight: '700', }, listContainer: { - padding: 20, + padding: isTablet ? 24 : 20, paddingTop: 8, + paddingBottom: isTablet ? 120 : 100, // Extra padding for tablet bottom nav }, downloadItem: { borderRadius: 16, - padding: 16, - marginBottom: 12, + padding: isTablet ? 20 : 16, + marginBottom: isTablet ? 16 : 12, flexDirection: 'row', alignItems: 'center', + minHeight: isTablet ? 165 : 152, // Accommodate tablet poster + padding shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, + posterContainer: { + width: POSTER_WIDTH, + height: POSTER_HEIGHT, + borderRadius: 8, + marginRight: isTablet ? 20 : 16, + position: 'relative', + overflow: 'hidden', + backgroundColor: '#333', + }, + poster: { + width: '100%', + height: '100%', + borderRadius: 8, + }, + statusOverlay: { + position: 'absolute', + top: 4, + right: 4, + width: 20, + height: 20, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.3, + shadowRadius: 2, + elevation: 2, + }, downloadContent: { flex: 1, - marginRight: 12, }, downloadHeader: { marginBottom: 12, @@ -627,6 +763,16 @@ const styles = StyleSheet.create({ sizeRow: { marginBottom: 6, }, + warningRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + marginBottom: 6, + }, + warningText: { + fontSize: 11, + fontWeight: '500', + }, progressInfo: { flexDirection: 'row', justifyContent: 'space-between', @@ -679,7 +825,8 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'center', justifyContent: 'center', - paddingHorizontal: 40, + paddingHorizontal: isTablet ? 64 : 40, + paddingBottom: isTablet ? 120 : 100, }, emptyIconContainer: { width: 96, @@ -690,16 +837,16 @@ const styles = StyleSheet.create({ marginBottom: 24, }, emptyTitle: { - fontSize: 24, + fontSize: isTablet ? 28 : 24, fontWeight: '700', marginBottom: 8, textAlign: 'center', }, emptySubtitle: { - fontSize: 16, + fontSize: isTablet ? 18 : 16, textAlign: 'center', - lineHeight: 24, - marginBottom: 32, + lineHeight: isTablet ? 28 : 24, + marginBottom: isTablet ? 40 : 32, }, exploreButton: { paddingHorizontal: 24, @@ -713,7 +860,7 @@ const styles = StyleSheet.create({ emptyFilterContainer: { alignItems: 'center', justifyContent: 'center', - paddingVertical: 60, + paddingVertical: isTablet ? 80 : 60, }, emptyFilterTitle: { fontSize: 18, diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index f5a0f5c..220c00c 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -282,8 +282,6 @@ const SettingsScreen: React.FC = () => { // Tablet-specific state const [selectedCategory, setSelectedCategory] = useState('account'); - const [downloadsDevUnlocked, setDownloadsDevUnlocked] = useState(false); - const [versionTapCount, setVersionTapCount] = useState(0); // States for dynamic content const [addonCount, setAddonCount] = useState(0); @@ -582,22 +580,20 @@ const SettingsScreen: React.FC = () => { )} isTablet={isTablet} /> - {downloadsDevUnlocked && ( - ( - updateSetting('enableDownloads', value)} - trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} - thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} - /> - )} - isTablet={isTablet} - /> - )} + ( + updateSetting('enableDownloads', value)} + trackColor={{ false: 'rgba(255,255,255,0.2)', true: currentTheme.colors.primary }} + thumbColor={settings?.enableDownloads ? '#fff' : '#f4f3f4'} + /> + )} + isTablet={isTablet} + /> { title="Version" description={getDisplayedAppVersion()} icon="info-outline" - onPress={() => { - if (downloadsDevUnlocked) return; - const next = versionTapCount + 1; - setVersionTapCount(next); - if (next >= 5) { - setDownloadsDevUnlocked(true); - setVersionTapCount(0); - openAlert('Developer option unlocked', 'Downloads toggle is now visible in Playback settings.'); - } - }} isLast={true} isTablet={isTablet} /> diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 13080b4..0e73f09 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -238,18 +238,14 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the Toast.success('Stream URL copied to clipboard!', 'bottom'); } else { // iOS uses custom alert - setTimeout(() => { - showAlert('Copied!', 'Stream URL has been copied to clipboard.'); - }, 50); + showAlert('Copied!', 'Stream URL has been copied to clipboard.'); } } catch (error) { // Fallback: show URL in alert if clipboard fails if (Platform.OS === 'android') { Toast.info(`Stream URL: ${stream.url}`, 'bottom'); } else { - setTimeout(() => { - showAlert('Stream URL', stream.url); - }, 50); + showAlert('Stream URL', stream.url); } } } @@ -331,19 +327,18 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the imdbId: parentImdbId || parent.imdbId || undefined, tmdbId: tmdbId, }); - Toast.success('Download started', 'bottom'); + showAlert('Download Started', 'Your download has been added to the queue.'); } catch {} }, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]); const isDebrid = streamInfo.isDebrid; return ( - - - onPress()} + activeOpacity={0.7} + > + - + {settings?.enableDownloads !== false && ( - { parentPosterUrl={episodeImage || metadata?.poster || undefined} providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))} parentId={id} - parentImdbId={imdbId} + parentImdbId={imdbId || undefined} /> )} diff --git a/src/styles/colors.ts b/src/styles/colors.ts index f335746..7cfd94f 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -49,8 +49,8 @@ export const colors = { surfaceVariant: 'rgba(255, 255, 255, 0.03)', // Material dark theme surface variant // Material Design elevation overlays - elevation1: 'rgba(255, 255, 255, 0.05)', - elevation2: 'rgba(255, 255, 255, 0.08)', + elevation1: 'rgba(255, 255, 255, 0.03)', + elevation2: 'rgba(255, 255, 255, 0.03)', elevation3: 'rgba(255, 255, 255, 0.11)', elevation4: 'rgba(255, 255, 255, 0.12)',