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) {