From edeb6ebe3cf7768e49f21bbcb8e2ac4cbc159871 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 5 Jan 2026 17:54:17 +0530 Subject: [PATCH] feat: added new poster like layout for continue watching card --- .../home/ContinueWatchingSection.tsx | 215 +++++++++++- src/hooks/useSettings.ts | 2 + .../ContinueWatchingSettingsScreen.tsx | 314 +++++++++++++----- 3 files changed, 448 insertions(+), 83 deletions(-) diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index f100ad92..237caf28 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -1002,8 +1002,128 @@ const ContinueWatchingSection = React.forwardRef((props, re setAlertVisible(true); }, [currentTheme.colors.error]); - // Memoized render function for continue watching items - const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( + // Compute poster dimensions for poster-style cards + const computedPosterWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return 180; + case 'largeTablet': + return 160; + case 'tablet': + return 140; + default: + return 120; + } + }, [deviceType]); + + const computedPosterHeight = useMemo(() => { + return computedPosterWidth * 1.5; // 2:3 aspect ratio + }, [computedPosterWidth]); + + // Memoized render function for poster-style continue watching items + const renderPosterStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( + handleContentPress(item)} + onLongPress={() => handleLongPress(item)} + delayLongPress={800} + > + {/* Poster Image */} + + + + {/* Gradient overlay */} + + + {/* Episode Info Overlay */} + {item.type === 'series' && item.season && item.episode && ( + + + S{item.season} E{item.episode} + + + )} + + {/* Up Next Badge */} + {item.type === 'series' && item.progress === 0 && ( + + UP NEXT + + )} + + {/* Progress Bar */} + {item.progress > 0 && ( + + + + + + )} + + {/* Delete Indicator Overlay */} + {deletingItemId === item.id && ( + + + + )} + + + {/* Title below poster */} + + + {item.name} + + {item.progress > 0 && ( + + {Math.round(item.progress)}% + + )} + + + ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedPosterWidth, computedPosterHeight, isTV, isLargeTablet, settings.posterBorderRadius]); + + // Memoized render function for wide-style continue watching items + const renderWideStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( ((props, re )} - ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]); + ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet, settings.posterBorderRadius]); + + // Choose the appropriate render function based on settings + const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => { + if (settings.continueWatchingCardStyle === 'poster') { + return renderPosterStyleItem({ item }); + } + return renderWideStyleItem({ item }); + }, [settings.continueWatchingCardStyle, renderPosterStyleItem, renderWideStyleItem]); // Memoized key extractor const keyExtractor = useCallback((item: ContinueWatchingItem) => `continue-${item.id}-${item.type}`, []); @@ -1421,6 +1549,87 @@ const styles = StyleSheet.create({ progressBar: { height: '100%', }, + // Poster-style card styles + posterContentItem: { + overflow: 'visible', + }, + posterImageContainer: { + width: '100%', + overflow: 'hidden', + position: 'relative', + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', + elevation: 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + }, + posterImage: { + width: '100%', + height: '100%', + }, + posterGradient: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: '50%', + }, + posterEpisodeOverlay: { + position: 'absolute', + bottom: 8, + left: 8, + backgroundColor: 'rgba(0,0,0,0.7)', + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 4, + }, + posterEpisodeText: { + color: '#FFFFFF', + fontWeight: '600', + }, + posterUpNextBadge: { + position: 'absolute', + top: 8, + right: 8, + paddingHorizontal: 6, + paddingVertical: 3, + borderRadius: 4, + }, + posterUpNextText: { + color: '#FFFFFF', + fontWeight: '700', + letterSpacing: 0.5, + }, + posterProgressContainer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + }, + posterProgressTrack: { + height: 4, + }, + posterProgressBar: { + height: '100%', + }, + posterTitleContainer: { + paddingHorizontal: 4, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + posterTitle: { + fontWeight: '600', + flex: 1, + lineHeight: 18, + }, + posterProgressLabel: { + fontWeight: '500', + marginLeft: 6, + }, }); export default React.memo(ContinueWatchingSection, (prevProps, nextProps) => { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index fd58bca2..18e5d9a7 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -100,6 +100,7 @@ export interface AppSettings { useCachedStreams: boolean; // Enable/disable direct player navigation from Continue Watching cache openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour) + continueWatchingCardStyle: 'wide' | 'poster'; // Card style: 'wide' (horizontal) or 'poster' (vertical) enableStreamsBackdrop: boolean; // Enable blurred backdrop background on StreamsScreen mobile useExternalPlayerForDownloads: boolean; // Enable/disable external player for downloaded content // Android MPV player settings @@ -186,6 +187,7 @@ export const DEFAULT_SETTINGS: AppSettings = { useCachedStreams: false, // Enable by default openMetadataScreenWhenCacheDisabled: true, // Default to StreamsScreen when cache disabled streamCacheTTL: 60 * 60 * 1000, // Default: 1 hour in milliseconds + continueWatchingCardStyle: 'wide', // Default to wide (horizontal) card style enableStreamsBackdrop: true, // Enable by default (new behavior) // Android MPV player settings videoPlayerEngine: 'auto', // Default to auto (ExoPlayer primary, MPV fallback) diff --git a/src/screens/ContinueWatchingSettingsScreen.tsx b/src/screens/ContinueWatchingSettingsScreen.tsx index 72bfdcaa..4242ab5a 100644 --- a/src/screens/ContinueWatchingSettingsScreen.tsx +++ b/src/screens/ContinueWatchingSettingsScreen.tsx @@ -53,7 +53,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => { if (Platform.OS === 'ios') { StatusBar.setHidden(false); } - } catch {} + } catch { } }, [colors.darkBackground]); const handleBack = useCallback(() => { @@ -97,22 +97,22 @@ const ContinueWatchingSettingsScreen: React.FC = () => { /> ); - const SettingItem = ({ - title, - description, - value, - onValueChange, - isLast = false - }: { - title: string; - description: string; - value: boolean; + const SettingItem = ({ + title, + description, + value, + onValueChange, + isLast = false + }: { + title: string; + description: string; + value: boolean; onValueChange: (value: boolean) => void; isLast?: boolean; }) => ( @@ -159,10 +159,10 @@ const ContinueWatchingSettingsScreen: React.FC = () => { return ( - + {/* Header */} - @@ -170,13 +170,13 @@ const ContinueWatchingSettingsScreen: React.FC = () => { Settings - + Continue Watching {/* Content */} - { PLAYBACK BEHAVIOR - handleUpdateSetting('useCachedStreams', value)} - isLast={!settings.useCachedStreams} - /> - {!settings.useCachedStreams && ( handleUpdateSetting('openMetadataScreenWhenCacheDisabled', value)} - isLast={true} + title="Use Cached Streams" + description="When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead." + value={settings.useCachedStreams} + onValueChange={(value) => handleUpdateSetting('useCachedStreams', value)} + isLast={!settings.useCachedStreams} /> - )} + {!settings.useCachedStreams && ( + handleUpdateSetting('openMetadataScreenWhenCacheDisabled', value)} + isLast={true} + /> + )} + + + + {/* Card Appearance Section */} + + CARD APPEARANCE + + + + Card Style + + + Choose how Continue Watching items appear on the home screen + + + handleUpdateSetting('continueWatchingCardStyle', 'wide')} + activeOpacity={0.7} + > + + + + + + + + + + + + Wide + + {settings.continueWatchingCardStyle === 'wide' && ( + + )} + + + handleUpdateSetting('continueWatchingCardStyle', 'poster')} + activeOpacity={0.7} + > + + + + + + + + Poster + + {settings.continueWatchingCardStyle === 'poster' && ( + + )} + + + @@ -207,80 +283,80 @@ const ContinueWatchingSettingsScreen: React.FC = () => { CACHE SETTINGS - - - Stream Cache Duration - - - How long to keep cached stream links before they expire - - - {TTL_OPTIONS.map((row, rowIndex) => ( - - {row.map((option) => ( - - ))} - - ))} + + + Stream Cache Duration + + + How long to keep cached stream links before they expire + + + {TTL_OPTIONS.map((row, rowIndex) => ( + + {row.map((option) => ( + + ))} + + ))} + - )} {settings.useCachedStreams && ( - - - - Important Note + + + + Important Note + + + + Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams. - - Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams. - - )} - - - - How it works + + + + How it works + + + + {settings.useCachedStreams ? ( + <> + • Streams are cached for your selected duration after playing{'\n'} + • Cached streams are validated before use{'\n'} + • If cache is invalid or expired, falls back to content screen{'\n'} + • "Use Cached Streams" controls direct player vs screen navigation{'\n'} + • "Open Metadata Screen" appears only when cached streams are disabled + + ) : ( + <> + • When cached streams are disabled, clicking Continue Watching items opens content screens{'\n'} + • "Open Metadata Screen" option controls which screen to open{'\n'} + • Metadata screen shows content details and allows manual stream selection{'\n'} + • Streams screen shows available streams for immediate playback + + )} - - {settings.useCachedStreams ? ( - <> - • Streams are cached for your selected duration after playing{'\n'} - • Cached streams are validated before use{'\n'} - • If cache is invalid or expired, falls back to content screen{'\n'} - • "Use Cached Streams" controls direct player vs screen navigation{'\n'} - • "Open Metadata Screen" appears only when cached streams are disabled - - ) : ( - <> - • When cached streams are disabled, clicking Continue Watching items opens content screens{'\n'} - • "Open Metadata Screen" option controls which screen to open{'\n'} - • Metadata screen shows content details and allows manual stream selection{'\n'} - • Streams screen shows available streams for immediate playback - - )} - - {/* Saved indicator */} - @@ -466,6 +542,84 @@ const createStyles = (colors: any) => StyleSheet.create({ fontSize: 14, lineHeight: 20, }, + // Card Style Selector Styles + cardStyleOptionsContainer: { + flexDirection: 'row', + width: '100%', + gap: 12, + }, + cardStyleOption: { + flex: 1, + alignItems: 'center', + paddingVertical: 16, + paddingHorizontal: 12, + borderRadius: 12, + borderWidth: 1, + position: 'relative', + }, + cardPreviewWide: { + flexDirection: 'row', + width: 100, + height: 60, + borderRadius: 6, + overflow: 'hidden', + marginBottom: 8, + alignSelf: 'center', + }, + cardPreviewImage: { + width: 40, + height: '100%', + borderTopLeftRadius: 6, + borderBottomLeftRadius: 6, + }, + cardPreviewContent: { + flex: 1, + padding: 4, + justifyContent: 'space-between', + }, + cardPreviewLine: { + height: 8, + borderRadius: 2, + }, + cardPreviewProgress: { + height: 4, + borderRadius: 2, + width: '100%', + }, + cardPreviewProgressFill: { + height: '100%', + borderRadius: 2, + }, + cardPreviewPoster: { + width: 44, + height: 60, + borderRadius: 6, + overflow: 'hidden', + marginBottom: 8, + position: 'relative', + }, + cardPreviewPosterImage: { + width: '100%', + height: '100%', + borderRadius: 6, + }, + cardPreviewPosterProgress: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 4, + }, + cardStyleLabel: { + fontSize: 14, + fontWeight: '600', + marginTop: 4, + }, + cardStyleCheck: { + position: 'absolute', + top: 8, + right: 8, + }, }); export default ContinueWatchingSettingsScreen;