diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 457d56fe..b74cdc86 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -23,6 +23,7 @@ import Animated, { withRepeat, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; +import { useTraktContext } from '../../contexts/TraktContext'; import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; @@ -52,6 +53,8 @@ interface HeroSectionProps { duration: number; lastUpdated: number; episodeId?: string; + traktSynced?: boolean; + traktProgress?: number; } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; @@ -196,21 +199,29 @@ const ActionButtons = React.memo(({ ); }); -// Ultra-optimized WatchProgress Component +// Enhanced WatchProgress Component with Trakt integration const WatchProgressDisplay = React.memo(({ watchProgress, type, getEpisodeDetails, animatedStyle, }: { - watchProgress: { currentTime: number; duration: number; lastUpdated: number; episodeId?: string } | null; + watchProgress: { + currentTime: number; + duration: number; + lastUpdated: number; + episodeId?: string; + traktSynced?: boolean; + traktProgress?: number; + } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; }) => { const { currentTheme } = useTheme(); + const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); - // Memoized progress calculation + // Memoized progress calculation with Trakt integration const progressData = useMemo(() => { if (!watchProgress || watchProgress.duration === 0) return null; @@ -225,13 +236,33 @@ const WatchProgressDisplay = React.memo(({ } } + // Enhanced display text with Trakt integration + let displayText = progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`; + let syncStatus = ''; + + // Show Trakt sync status if user is authenticated + if (isTraktAuthenticated) { + if (watchProgress.traktSynced) { + syncStatus = ' • Synced with Trakt'; + // If we have specific Trakt progress that differs from local, mention it + if (watchProgress.traktProgress !== undefined && + Math.abs(progressPercent - watchProgress.traktProgress) > 5) { + displayText = `${Math.round(progressPercent)}% watched (${Math.round(watchProgress.traktProgress)}% on Trakt)`; + } + } else { + syncStatus = ' • Sync pending'; + } + } + return { progressPercent, formattedTime, episodeInfo, - displayText: progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched` + displayText, + syncStatus, + isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated }; - }, [watchProgress, type, getEpisodeDetails]); + }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated]); if (!progressData) return null; @@ -243,13 +274,26 @@ const WatchProgressDisplay = React.memo(({ styles.watchProgressFill, { width: `${progressData.progressPercent}%`, - backgroundColor: currentTheme.colors.primary + backgroundColor: progressData.isTraktSynced + ? '#E50914' // Netflix red for Trakt synced content + : currentTheme.colors.primary } ]} /> + {/* Trakt sync indicator */} + {progressData.isTraktSynced && ( + + + + )} {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime} + {progressData.syncStatus} ); @@ -280,6 +324,7 @@ const HeroSection: React.FC = ({ setLogoLoadError, }) => { const { currentTheme } = useTheme(); + const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); // Enhanced state for smooth image loading const [imageError, setImageError] = useState(false); @@ -470,7 +515,7 @@ const HeroSection: React.FC = ({ - {/* Optimized Watch Progress */} + {/* Enhanced Watch Progress with Trakt integration */} Promise; markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise; markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise; + forceSyncTraktProgress?: () => Promise; } const TraktContext = createContext(undefined); diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index 027205c6..7b4e71b2 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -13,15 +13,20 @@ export function useTraktIntegration() { // Check authentication status const checkAuthStatus = useCallback(async () => { + logger.log('[useTraktIntegration] checkAuthStatus called'); setIsLoading(true); try { const authenticated = await traktService.isAuthenticated(); + logger.log(`[useTraktIntegration] Authentication check result: ${authenticated}`); setIsAuthenticated(authenticated); if (authenticated) { + logger.log('[useTraktIntegration] User is authenticated, fetching profile...'); const profile = await traktService.getUserProfile(); + logger.log(`[useTraktIntegration] User profile: ${profile.username}`); setUserProfile(profile); } else { + logger.log('[useTraktIntegration] User is not authenticated'); setUserProfile(null); } @@ -187,10 +192,18 @@ export function useTraktIntegration() { // Get playback progress from Trakt const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise => { - if (!isAuthenticated) return []; + logger.log(`[useTraktIntegration] getTraktPlaybackProgress called - isAuthenticated: ${isAuthenticated}, type: ${type || 'all'}`); + + if (!isAuthenticated) { + logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated'); + return []; + } try { - return await traktService.getPlaybackProgress(type); + logger.log('[useTraktIntegration] Calling traktService.getPlaybackProgress...'); + const result = await traktService.getPlaybackProgress(type); + logger.log(`[useTraktIntegration] traktService.getPlaybackProgress returned ${result.length} items`); + return result; } catch (error) { logger.error('[useTraktIntegration] Error getting playback progress:', error); return []; @@ -260,10 +273,22 @@ export function useTraktIntegration() { // Fetch and merge Trakt progress with local progress const fetchAndMergeTraktProgress = useCallback(async (): Promise => { - if (!isAuthenticated) return false; + logger.log(`[useTraktIntegration] fetchAndMergeTraktProgress called - isAuthenticated: ${isAuthenticated}`); + + if (!isAuthenticated) { + logger.log('[useTraktIntegration] Not authenticated, skipping Trakt progress fetch'); + return false; + } try { + logger.log('[useTraktIntegration] Fetching Trakt playback progress...'); const traktProgress = await getTraktPlaybackProgress(); + logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items`); + + 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 + } for (const item of traktProgress) { try { @@ -274,14 +299,18 @@ 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}%`); } else if (item.type === 'episode' && item.show && item.episode) { id = item.show.ids.imdb; type = 'series'; - episodeId = `S${item.episode.season}E${item.episode.number}`; + 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}%`); } else { + logger.warn(`[useTraktIntegration] Skipping invalid Trakt item:`, item); continue; } + logger.log(`[useTraktIntegration] Merging progress for ${type} ${id}: ${item.progress}% from ${item.paused_at}`); await storageService.mergeWithTraktProgress( id, type, @@ -294,7 +323,7 @@ export function useTraktIntegration() { } } - logger.log(`[useTraktIntegration] Merged ${traktProgress.length} Trakt progress entries`); + logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} Trakt progress entries`); return true; } catch (error) { logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error); @@ -314,14 +343,47 @@ export function useTraktIntegration() { } }, [isAuthenticated, loadWatchedItems]); - // Auto-sync when authenticated changes + // Auto-sync when authenticated changes OR when auth status is refreshed useEffect(() => { if (isAuthenticated) { // Fetch Trakt progress and merge with local - fetchAndMergeTraktProgress(); + logger.log('[useTraktIntegration] User authenticated, fetching Trakt progress to replace local data'); + fetchAndMergeTraktProgress().then((success) => { + if (success) { + logger.log('[useTraktIntegration] Trakt progress merged successfully - local data replaced with Trakt data'); + } else { + logger.warn('[useTraktIntegration] Failed to merge Trakt progress'); + } + // Small delay to ensure storage subscribers are notified + setTimeout(() => { + logger.log('[useTraktIntegration] Trakt progress merge completed, UI should refresh'); + }, 100); + }); } }, [isAuthenticated, fetchAndMergeTraktProgress]); + // Trigger sync when auth status is manually refreshed (for login scenarios) + useEffect(() => { + if (isAuthenticated) { + logger.log('[useTraktIntegration] Auth status refresh detected, triggering Trakt progress merge'); + fetchAndMergeTraktProgress().then((success) => { + if (success) { + logger.log('[useTraktIntegration] Trakt progress merged after manual auth refresh'); + } + }); + } + }, [lastAuthCheck, isAuthenticated, fetchAndMergeTraktProgress]); + + // Manual force sync function for testing/troubleshooting + const forceSyncTraktProgress = useCallback(async (): Promise => { + logger.log('[useTraktIntegration] Manual force sync triggered'); + if (!isAuthenticated) { + logger.log('[useTraktIntegration] Cannot force sync - not authenticated'); + return false; + } + return await fetchAndMergeTraktProgress(); + }, [isAuthenticated, fetchAndMergeTraktProgress]); + return { isAuthenticated, isLoading, @@ -341,6 +403,7 @@ export function useTraktIntegration() { syncProgress, // legacy getTraktPlaybackProgress, syncAllProgress, - fetchAndMergeTraktProgress + fetchAndMergeTraktProgress, + forceSyncTraktProgress // For manual testing }; } \ No newline at end of file diff --git a/src/hooks/useWatchProgress.ts b/src/hooks/useWatchProgress.ts index 7c71539b..0dcc9df0 100644 --- a/src/hooks/useWatchProgress.ts +++ b/src/hooks/useWatchProgress.ts @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { useFocusEffect } from '@react-navigation/native'; +import { useTraktContext } from '../contexts/TraktContext'; import { logger } from '../utils/logger'; import { storageService } from '../services/storageService'; @@ -8,6 +9,8 @@ interface WatchProgressData { duration: number; lastUpdated: number; episodeId?: string; + traktSynced?: boolean; + traktProgress?: number; } export const useWatchProgress = ( @@ -17,6 +20,7 @@ export const useWatchProgress = ( episodes: any[] = [] ) => { const [watchProgress, setWatchProgress] = useState(null); + const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); // Function to get episode details from episodeId const getEpisodeDetails = useCallback((episodeId: string): { seasonNumber: string; episodeNumber: string; episodeName: string } | null => { @@ -52,7 +56,7 @@ export const useWatchProgress = ( return null; }, [episodes]); - // Load watch progress + // Enhanced load watch progress with Trakt integration const loadWatchProgress = useCallback(async () => { try { if (id && type) { @@ -119,9 +123,20 @@ export const useWatchProgress = ( `${id}:${nextEpisode.season_number}:${nextEpisode.episode_number}`; const nextProgress = await storageService.getWatchProgress(id, type, nextEpisodeId); if (nextProgress) { - setWatchProgress({ ...nextProgress, episodeId: nextEpisodeId }); + setWatchProgress({ + ...nextProgress, + episodeId: nextEpisodeId, + traktSynced: nextProgress.traktSynced, + traktProgress: nextProgress.traktProgress + }); } else { - setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: nextEpisodeId }); + setWatchProgress({ + currentTime: 0, + duration: 0, + lastUpdated: Date.now(), + episodeId: nextEpisodeId, + traktSynced: false + }); } return; } @@ -132,7 +147,12 @@ export const useWatchProgress = ( } // If current episode is not finished, show its progress - setWatchProgress({ ...progress, episodeId }); + setWatchProgress({ + ...progress, + episodeId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } else { setWatchProgress(null); } @@ -151,9 +171,20 @@ export const useWatchProgress = ( `${id}:${unfinishedEpisode.season_number}:${unfinishedEpisode.episode_number}`; const progress = await storageService.getWatchProgress(id, type, epId); if (progress) { - setWatchProgress({ ...progress, episodeId: epId }); + setWatchProgress({ + ...progress, + episodeId: epId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } else { - setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: epId }); + setWatchProgress({ + currentTime: 0, + duration: 0, + lastUpdated: Date.now(), + episodeId: epId, + traktSynced: false + }); } } else { setWatchProgress(null); @@ -167,7 +198,12 @@ export const useWatchProgress = ( if (progressPercent >= 95) { setWatchProgress(null); } else { - setWatchProgress({ ...progress, episodeId }); + setWatchProgress({ + ...progress, + episodeId, + traktSynced: progress.traktSynced, + traktProgress: progress.traktProgress + }); } } else { setWatchProgress(null); @@ -180,7 +216,7 @@ export const useWatchProgress = ( } }, [id, type, episodeId, episodes]); - // Function to get play button text based on watch progress + // Enhanced function to get play button text with Trakt awareness const getPlayButtonText = useCallback(() => { if (!watchProgress || watchProgress.currentTime <= 0) { return 'Play'; @@ -192,9 +228,21 @@ export const useWatchProgress = ( return 'Play'; } + // If we have Trakt data and it differs significantly from local, show "Resume" + // but the UI will show the discrepancy return 'Resume'; }, [watchProgress]); + // Subscribe to storage changes for real-time updates + useEffect(() => { + const unsubscribe = storageService.subscribeToWatchProgressUpdates(() => { + logger.log('[useWatchProgress] Storage updated, reloading progress'); + loadWatchProgress(); + }); + + return unsubscribe; + }, [loadWatchProgress]); + // Initial load useEffect(() => { loadWatchProgress(); @@ -207,6 +255,16 @@ export const useWatchProgress = ( }, [loadWatchProgress]) ); + // Re-load when Trakt authentication status changes + useEffect(() => { + if (isTraktAuthenticated !== undefined) { + // Small delay to ensure Trakt context is fully initialized + setTimeout(() => { + loadWatchProgress(); + }, 100); + } + }, [isTraktAuthenticated, loadWatchProgress]); + return { watchProgress, getEpisodeDetails, diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index d9f9e474..ec3ad88e 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -330,18 +330,18 @@ const TraktSettingsScreen: React.FC = () => { - - Auto-sync playback progress - - + + Auto-sync playback progress + + Automatically sync watch progress to Trakt - + localProgress.lastUpdated; + // Always prioritize Trakt progress when merging + const localProgressPercent = (localProgress.currentTime / localProgress.duration) * 100; - if (shouldUseTraktProgress && localProgress.duration > 0) { + if (localProgress.duration > 0) { + // Use Trakt progress, keeping the existing duration const updatedProgress: WatchProgress = { ...localProgress, currentTime: (traktProgress / 100) * localProgress.duration, @@ -221,9 +222,20 @@ class StorageService { traktProgress }; await this.setWatchProgress(id, type, updatedProgress, episodeId); + logger.log(`[StorageService] Replaced local progress (${localProgressPercent.toFixed(1)}%) with Trakt progress (${traktProgress}%)`); } else { - // Local is newer, just mark as needing sync - await this.updateTraktSyncStatus(id, type, false, undefined, episodeId); + // If no duration, estimate it from Trakt progress + const estimatedDuration = traktProgress > 0 ? (100 / traktProgress) * 100 : 3600; + const updatedProgress: WatchProgress = { + currentTime: (traktProgress / 100) * estimatedDuration, + duration: estimatedDuration, + lastUpdated: traktTimestamp, + traktSynced: true, + traktLastSynced: Date.now(), + traktProgress + }; + await this.setWatchProgress(id, type, updatedProgress, episodeId); + logger.log(`[StorageService] Replaced local progress (${localProgressPercent.toFixed(1)}%) with Trakt progress (${traktProgress}%) - estimated duration`); } } } catch (error) {