diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index b74cdc86..7f36a029 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -77,7 +77,9 @@ const ActionButtons = React.memo(({ id, navigation, playButtonText, - animatedStyle + animatedStyle, + isWatched, + watchProgress }: { handleShowStreams: () => void; toggleLibrary: () => void; @@ -87,6 +89,8 @@ const ActionButtons = React.memo(({ navigation: any; playButtonText: string; animatedStyle: any; + isWatched: boolean; + watchProgress: any; }) => { const { currentTheme } = useTheme(); @@ -122,19 +126,48 @@ const ActionButtons = React.memo(({ } }, [id, navigation]); + // Determine play button style and text based on watched status + const playButtonStyle = useMemo(() => { + if (isWatched) { + return [styles.actionButton, styles.playButton, styles.watchedPlayButton]; + } + return [styles.actionButton, styles.playButton]; + }, [isWatched]); + + const playButtonTextStyle = useMemo(() => { + if (isWatched) { + return [styles.playButtonText, styles.watchedPlayButtonText]; + } + return styles.playButtonText; + }, [isWatched]); + + const finalPlayButtonText = useMemo(() => { + if (isWatched) { + return 'Watch Again'; + } + return playButtonText; + }, [isWatched, playButtonText]); + return ( - {playButtonText} + {finalPlayButtonText} + + {/* Subtle watched indicator in play button */} + {isWatched && ( + + + + )} { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; + isWatched: boolean; }) => { const { currentTheme } = useTheme(); - const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); + const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext(); + + // Handle manual Trakt sync + const handleTraktSync = useMemo(() => async () => { + if (isTraktAuthenticated && forceSyncTraktProgress) { + logger.log('[HeroSection] Manual Trakt sync requested'); + try { + const success = await forceSyncTraktProgress(); + logger.log(`[HeroSection] Manual Trakt sync ${success ? 'successful' : 'failed'}`); + } catch (error) { + logger.error('[HeroSection] Manual Trakt sync error:', error); + } + } + }, [isTraktAuthenticated, forceSyncTraktProgress]); // Memoized progress calculation with Trakt integration const progressData = useMemo(() => { + // If content is fully watched, show watched status instead of progress + if (isWatched) { + let episodeInfo = ''; + if (type === 'series' && watchProgress?.episodeId) { + const details = getEpisodeDetails(watchProgress.episodeId); + if (details) { + episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`; + } + } + + const watchedDate = watchProgress?.lastUpdated + ? new Date(watchProgress.lastUpdated).toLocaleDateString() + : new Date().toLocaleDateString(); + + // Determine if watched via Trakt or local + const watchedViaTrakt = isTraktAuthenticated && + watchProgress?.traktProgress !== undefined && + watchProgress.traktProgress >= 95; + + return { + progressPercent: 100, + formattedTime: watchedDate, + episodeInfo, + displayText: watchedViaTrakt ? 'Watched on Trakt' : 'Watched', + syncStatus: isTraktAuthenticated && watchProgress?.traktSynced ? '' : '', // Clean look for watched + isTraktSynced: watchProgress?.traktSynced && isTraktAuthenticated, + isWatched: true + }; + } + if (!watchProgress || watchProgress.duration === 0) return null; - const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; + // Determine which progress to show - prioritize Trakt if available and authenticated + let progressPercent; + let isUsingTraktProgress = false; + + if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) { + progressPercent = watchProgress.traktProgress; + isUsingTraktProgress = true; + } else { + progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; + } const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString(); let episodeInfo = ''; @@ -242,7 +329,12 @@ const WatchProgressDisplay = React.memo(({ // Show Trakt sync status if user is authenticated if (isTraktAuthenticated) { - if (watchProgress.traktSynced) { + if (isUsingTraktProgress) { + syncStatus = ' • Using Trakt progress'; + if (watchProgress.traktSynced) { + syncStatus = ' • Synced with Trakt'; + } + } else if (watchProgress.traktSynced) { syncStatus = ' • Synced with Trakt'; // If we have specific Trakt progress that differs from local, mention it if (watchProgress.traktProgress !== undefined && @@ -260,9 +352,10 @@ const WatchProgressDisplay = React.memo(({ episodeInfo, displayText, syncStatus, - isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated + isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated, + isWatched: false }; - }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated]); + }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated, isWatched]); if (!progressData) return null; @@ -274,14 +367,26 @@ const WatchProgressDisplay = React.memo(({ styles.watchProgressFill, { width: `${progressData.progressPercent}%`, - backgroundColor: progressData.isTraktSynced - ? '#E50914' // Netflix red for Trakt synced content - : currentTheme.colors.primary + backgroundColor: progressData.isWatched + ? '#666' // Subtle gray for completed + : progressData.isTraktSynced + ? '#E50914' // Netflix red for Trakt synced content + : currentTheme.colors.primary } ]} /> - {/* Trakt sync indicator */} - {progressData.isTraktSynced && ( + {/* Subtle watched indicator */} + {progressData.isWatched && ( + + + + )} + {/* Trakt sync indicator for non-watched content */} + {progressData.isTraktSynced && !progressData.isWatched && ( )} - - {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime} - {progressData.syncStatus} - + + + {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime} + {progressData.syncStatus} + + + {/* Manual Trakt sync button */} + {isTraktAuthenticated && forceSyncTraktProgress && ( + + + + )} + ); }); @@ -452,6 +577,25 @@ const HeroSection: React.FC = ({ // Memoized play button text const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]); + // Calculate if content is watched (>=95% progress) - check both local and Trakt progress + const isWatched = useMemo(() => { + if (!watchProgress) return false; + + // Check Trakt progress first if available and user is authenticated + if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) { + const traktWatched = watchProgress.traktProgress >= 95; + logger.log(`[HeroSection] Trakt authenticated: ${isTraktAuthenticated}, Trakt progress: ${watchProgress.traktProgress}%, Watched: ${traktWatched}`); + return traktWatched; + } + + // Fall back to local progress + if (watchProgress.duration === 0) return false; + const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; + const localWatched = progressPercent >= 95; + logger.log(`[HeroSection] Local progress: ${progressPercent.toFixed(1)}%, Watched: ${localWatched}`); + return localWatched; + }, [watchProgress, isTraktAuthenticated]); + return ( {/* Optimized Background */} @@ -521,6 +665,7 @@ const HeroSection: React.FC = ({ type={type} getEpisodeDetails={getEpisodeDetails} animatedStyle={watchProgressAnimatedStyle} + isWatched={isWatched} /> {/* Optimized Genres */} @@ -540,6 +685,8 @@ const HeroSection: React.FC = ({ navigation={navigation} playButtonText={playButtonText} animatedStyle={buttonsAnimatedStyle} + isWatched={isWatched} + watchProgress={watchProgress} /> @@ -623,6 +770,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', width: '100%', + position: 'relative', }, actionButton: { flexDirection: 'row', @@ -697,11 +845,32 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + watchedProgressIndicator: { + position: 'absolute', + right: 2, + top: -1, + bottom: -1, + width: 10, + alignItems: 'center', + justifyContent: 'center', + }, + watchProgressTextContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, watchProgressText: { fontSize: 11, textAlign: 'center', opacity: 0.85, - letterSpacing: 0.1 + letterSpacing: 0.1, + flex: 1, + }, + traktSyncButton: { + padding: 4, + borderRadius: 12, + backgroundColor: 'rgba(255,255,255,0.1)', }, blurBackground: { position: 'absolute', @@ -737,6 +906,33 @@ const styles = StyleSheet.create({ borderRadius: 25, backgroundColor: 'rgba(255,255,255,0.15)', }, + watchedIndicator: { + position: 'absolute', + top: 4, + right: 4, + backgroundColor: 'rgba(0,0,0,0.6)', + borderRadius: 8, + width: 16, + height: 16, + alignItems: 'center', + justifyContent: 'center', + }, + watchedPlayButton: { + backgroundColor: '#1e1e1e', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.3)', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 4, + }, + watchedPlayButtonText: { + color: '#fff', + fontWeight: '700', + marginLeft: 6, + fontSize: 15, + }, }); export default React.memo(HeroSection); \ No newline at end of file diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index 7b4e71b2..27c414b5 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -281,15 +281,16 @@ export function useTraktIntegration() { } try { - logger.log('[useTraktIntegration] Fetching Trakt playback progress...'); - const traktProgress = await getTraktPlaybackProgress(); - logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items`); + // Fetch both playback progress and recently watched movies + logger.log('[useTraktIntegration] Fetching Trakt playback progress and watched movies...'); + const [traktProgress, watchedMovies] = await Promise.all([ + getTraktPlaybackProgress(), + traktService.getWatchedMovies() + ]); - if (traktProgress.length === 0) { - logger.log('[useTraktIntegration] No Trakt progress found - user may not have any content in progress'); - return true; // Not an error, just no data - } + logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items, ${watchedMovies.length} watched movies`); + // Process playback progress (in-progress items) for (const item of traktProgress) { try { let id: string; @@ -299,14 +300,14 @@ export function useTraktIntegration() { if (item.type === 'movie' && item.movie) { id = item.movie.ids.imdb; type = 'movie'; - logger.log(`[useTraktIntegration] Processing Trakt movie: ${item.movie.title} (${id}) - ${item.progress}%`); + logger.log(`[useTraktIntegration] Processing Trakt movie progress: ${item.movie.title} (${id}) - ${item.progress}%`); } else if (item.type === 'episode' && item.show && item.episode) { id = item.show.ids.imdb; type = 'series'; episodeId = `${id}:${item.episode.season}:${item.episode.number}`; - logger.log(`[useTraktIntegration] Processing Trakt episode: ${item.show.title} S${item.episode.season}E${item.episode.number} (${id}) - ${item.progress}%`); + logger.log(`[useTraktIntegration] Processing Trakt episode progress: ${item.show.title} S${item.episode.season}E${item.episode.number} (${id}) - ${item.progress}%`); } else { - logger.warn(`[useTraktIntegration] Skipping invalid Trakt item:`, item); + logger.warn(`[useTraktIntegration] Skipping invalid Trakt progress item:`, item); continue; } @@ -323,7 +324,27 @@ export function useTraktIntegration() { } } - logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} Trakt progress entries`); + // Process watched movies (100% completed) + for (const movie of watchedMovies) { + try { + if (movie.movie?.ids?.imdb) { + const id = movie.movie.ids.imdb; + const watchedAt = movie.last_watched_at; + logger.log(`[useTraktIntegration] Processing watched movie: ${movie.movie.title} (${id}) - 100% watched on ${watchedAt}`); + + await storageService.mergeWithTraktProgress( + id, + 'movie', + 100, // 100% progress for watched items + watchedAt + ); + } + } catch (error) { + logger.error('[useTraktIntegration] Error merging watched movie:', error); + } + } + + logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} progress items + ${watchedMovies.length} watched movies`); return true; } catch (error) { logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error); @@ -362,6 +383,24 @@ export function useTraktIntegration() { } }, [isAuthenticated, fetchAndMergeTraktProgress]); + // Periodic sync - check for updates every 2 minutes when authenticated + useEffect(() => { + if (!isAuthenticated) return; + + const intervalId = setInterval(() => { + logger.log('[useTraktIntegration] Periodic Trakt sync check'); + fetchAndMergeTraktProgress().then((success) => { + if (success) { + logger.log('[useTraktIntegration] Periodic sync completed successfully'); + } + }).catch(error => { + logger.error('[useTraktIntegration] Periodic sync failed:', error); + }); + }, 2 * 60 * 1000); // 2 minutes + + return () => clearInterval(intervalId); + }, [isAuthenticated, fetchAndMergeTraktProgress]); + // Trigger sync when auth status is manually refreshed (for login scenarios) useEffect(() => { if (isAuthenticated) { diff --git a/src/hooks/useWatchProgress.ts b/src/hooks/useWatchProgress.ts index 0dcc9df0..b17e4345 100644 --- a/src/hooks/useWatchProgress.ts +++ b/src/hooks/useWatchProgress.ts @@ -91,62 +91,8 @@ export const useWatchProgress = ( if (episodeId) { const progress = await storageService.getWatchProgress(id, type, episodeId); if (progress) { - const progressPercent = (progress.currentTime / progress.duration) * 100; - - // If current episode is finished (≥95%), try to find next unwatched episode - if (progressPercent >= 95) { - const currentEpNum = getEpisodeNumber(episodeId); - if (currentEpNum && episodes.length > 0) { - // Find the next episode - const nextEpisode = episodes.find(ep => { - // First check in same season - if (ep.season_number === currentEpNum.season && ep.episode_number > currentEpNum.episode) { - const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`; - const epProgress = seriesProgresses.find(p => p.episodeId === epId); - if (!epProgress) return true; - const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100; - return percent < 95; - } - // Then check next seasons - if (ep.season_number > currentEpNum.season) { - const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`; - const epProgress = seriesProgresses.find(p => p.episodeId === epId); - if (!epProgress) return true; - const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100; - return percent < 95; - } - return false; - }); - - if (nextEpisode) { - const nextEpisodeId = nextEpisode.stremioId || - `${id}:${nextEpisode.season_number}:${nextEpisode.episode_number}`; - const nextProgress = await storageService.getWatchProgress(id, type, nextEpisodeId); - if (nextProgress) { - setWatchProgress({ - ...nextProgress, - episodeId: nextEpisodeId, - traktSynced: nextProgress.traktSynced, - traktProgress: nextProgress.traktProgress - }); - } else { - setWatchProgress({ - currentTime: 0, - duration: 0, - lastUpdated: Date.now(), - episodeId: nextEpisodeId, - traktSynced: false - }); - } - return; - } - } - // If no next episode found or current episode is finished, show no progress - setWatchProgress(null); - return; - } - - // If current episode is not finished, show its progress + // Always show the current episode progress when viewing it specifically + // This allows HeroSection to properly display watched state setWatchProgress({ ...progress, episodeId, @@ -194,17 +140,14 @@ export const useWatchProgress = ( // For movies const progress = await storageService.getWatchProgress(id, type, episodeId); if (progress && progress.currentTime > 0) { - const progressPercent = (progress.currentTime / progress.duration) * 100; - if (progressPercent >= 95) { - setWatchProgress(null); - } else { - setWatchProgress({ - ...progress, - episodeId, - traktSynced: progress.traktSynced, - traktProgress: progress.traktProgress - }); - } + // Always show progress data, even if watched (≥95%) + // The HeroSection will handle the "watched" state display + setWatchProgress({ + ...progress, + episodeId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } else { setWatchProgress(null); }