From dbbee06a559da1ac0484b5908c814276569c2bc5 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 15 Dec 2025 01:36:10 +0530 Subject: [PATCH] bug fixes android and ios --- src/components/player/AndroidVideoPlayer.tsx | 105 ++++++------- src/components/player/KSPlayerCore.tsx | 101 ++++++------- src/hooks/useSettings.ts | 2 +- src/hooks/useTraktIntegration.ts | 147 +++++++++---------- src/hooks/useWatchProgress.ts | 75 ++++++---- src/navigation/AppNavigator.tsx | 12 +- 6 files changed, 228 insertions(+), 214 deletions(-) diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index e52a21dc..d27fd0d5 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -861,18 +861,21 @@ const AndroidVideoPlayer: React.FC = () => { // Re-apply immersive mode on layout changes to keep system bars hidden enableImmersiveMode(); }); - const initializePlayer = async () => { - StatusBar.setHidden(true, 'none'); - enableImmersiveMode(); - startOpeningAnimation(); - // Initialize current volume and brightness levels - // Volume starts at 1.0 (full volume) - React Native Video handles this natively - setVolume(1.0); - if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Initial volume: 1.0 (native)`); - } + // Immediate player setup - UI critical + StatusBar.setHidden(true, 'none'); + enableImmersiveMode(); + startOpeningAnimation(); + // Initialize volume immediately (no async) + setVolume(1.0); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Initial volume: 1.0 (native)`); + } + + // Defer brightness initialization until after navigation animation completes + // This prevents sluggish player entry + const brightnessTask = InteractionManager.runAfterInteractions(async () => { try { // Capture Android system brightness and mode to restore later if (Platform.OS === 'android') { @@ -900,10 +903,11 @@ const AndroidVideoPlayer: React.FC = () => { // Fallback to 1.0 if brightness API fails setBrightness(1.0); } - }; - initializePlayer(); + }); + return () => { subscription?.remove(); + brightnessTask.cancel(); disableImmersiveMode(); }; }, []); @@ -1772,49 +1776,46 @@ const AndroidVideoPlayer: React.FC = () => { } }; - await restoreSystemBrightness(); + // Don't await brightness restoration - do it in background + restoreSystemBrightness(); - // Navigate immediately without delay - ScreenOrientation.unlockAsync().then(() => { - // On tablets keep rotation unlocked; on phones, return to portrait - const { width: dw, height: dh } = Dimensions.get('window'); - const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true); - if (!isTablet) { - setTimeout(() => { - ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { }); - }, 50); - } else { - ScreenOrientation.unlockAsync().catch(() => { }); - } - disableImmersiveMode(); + // Disable immersive mode immediately (synchronous) + disableImmersiveMode(); - // Simple back navigation (StreamsScreen should be below Player) - if ((navigation as any).canGoBack && (navigation as any).canGoBack()) { - (navigation as any).goBack(); - } else { - // Fallback to Streams if stack isn't present - (navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true }); - } - }).catch(() => { - // Fallback: still try to restore portrait on phones then navigate - const { width: dw, height: dh } = Dimensions.get('window'); - const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true); - if (!isTablet) { - setTimeout(() => { - ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { }); - }, 50); - } else { - ScreenOrientation.unlockAsync().catch(() => { }); - } - disableImmersiveMode(); + // Navigate IMMEDIATELY - don't wait for orientation changes + if ((navigation as any).canGoBack && (navigation as any).canGoBack()) { + (navigation as any).goBack(); + } else { + // Fallback to Streams if stack isn't present + (navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true }); + } - // Simple back navigation fallback path - if ((navigation as any).canGoBack && (navigation as any).canGoBack()) { - (navigation as any).goBack(); - } else { - (navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true }); - } - }); + // Fire orientation changes in background - don't await + ScreenOrientation.unlockAsync() + .then(() => { + // On tablets keep rotation unlocked; on phones, return to portrait + const { width: dw, height: dh } = Dimensions.get('window'); + const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true); + if (!isTablet) { + setTimeout(() => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { }); + }, 50); + } else { + ScreenOrientation.unlockAsync().catch(() => { }); + } + }) + .catch(() => { + // Fallback: still try to restore portrait on phones + const { width: dw, height: dh } = Dimensions.get('window'); + const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true); + if (!isTablet) { + setTimeout(() => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { }); + }, 50); + } else { + ScreenOrientation.unlockAsync().catch(() => { }); + } + }); // Send Trakt sync in background (don't await) const backgroundSync = async () => { diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index ede1699d..9729ac2d 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -589,20 +589,19 @@ const KSPlayerCore: React.FC = () => { // Force landscape orientation after opening animation completes useEffect(() => { - const lockOrientation = async () => { - try { - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - if (__DEV__) logger.log('[VideoPlayer] Locked to landscape orientation'); - } catch (error) { - logger.warn('[VideoPlayer] Failed to lock orientation:', error); - } - }; - - // Lock orientation after opening animation completes to prevent glitches + // Defer orientation lock until after navigation animation to prevent sluggishness if (isOpeningAnimationComplete) { - lockOrientation(); + const task = InteractionManager.runAfterInteractions(() => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE) + .then(() => { + if (__DEV__) logger.log('[VideoPlayer] Locked to landscape orientation'); + }) + .catch((error) => { + logger.warn('[VideoPlayer] Failed to lock orientation:', error); + }); + }); + return () => task.cancel(); } - return () => { // Do not unlock orientation here; we unlock explicitly on close to avoid mid-transition flips }; @@ -616,21 +615,24 @@ const KSPlayerCore: React.FC = () => { enableImmersiveMode(); } }); - const initializePlayer = async () => { - StatusBar.setHidden(true, 'none'); - // Enable immersive mode after opening animation to prevent glitches - if (isOpeningAnimationComplete) { - enableImmersiveMode(); - } - startOpeningAnimation(); - // Initialize current volume and brightness levels - // Volume starts at 100 (full volume) for KSPlayer - setVolume(100); - if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Initial volume: 100 (KSPlayer native)`); - } + // Immediate player setup - UI critical + StatusBar.setHidden(true, 'none'); + // Enable immersive mode after opening animation to prevent glitches + if (isOpeningAnimationComplete) { + enableImmersiveMode(); + } + startOpeningAnimation(); + // Initialize volume immediately (no async) + setVolume(100); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Initial volume: 100 (KSPlayer native)`); + } + + // Defer brightness initialization until after navigation animation completes + // This prevents sluggish player entry + const brightnessTask = InteractionManager.runAfterInteractions(async () => { try { const currentBrightness = await Brightness.getBrightnessAsync(); setBrightness(currentBrightness); @@ -642,10 +644,11 @@ const KSPlayerCore: React.FC = () => { // Fallback to 1.0 if brightness API fails setBrightness(1.0); } - }; - initializePlayer(); + }); + return () => { subscription?.remove(); + brightnessTask.cancel(); disableImmersiveMode(); }; }, [isOpeningAnimationComplete]); @@ -1381,32 +1384,32 @@ const KSPlayerCore: React.FC = () => { logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`); // Cleanup and navigate back immediately without delay - const cleanup = async () => { - try { - // Unlock orientation first - await ScreenOrientation.unlockAsync(); - logger.log('[VideoPlayer] Orientation unlocked'); - } catch (orientationError) { - logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError); - } - - // On iOS tablets, keep rotation unlocked; on phones, return to portrait - if (Platform.OS === 'ios') { - const { width: dw, height: dh } = Dimensions.get('window'); - const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768; - setTimeout(() => { - if (isTablet) { - ScreenOrientation.unlockAsync().catch(() => { }); - } else { - ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { }); + const cleanup = () => { + // Fire orientation changes in background - don't await them + ScreenOrientation.unlockAsync() + .then(() => { + logger.log('[VideoPlayer] Orientation unlocked'); + // On iOS tablets, keep rotation unlocked; on phones, return to portrait + if (Platform.OS === 'ios') { + const { width: dw, height: dh } = Dimensions.get('window'); + const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768; + setTimeout(() => { + if (isTablet) { + ScreenOrientation.unlockAsync().catch(() => { }); + } else { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { }); + } + }, 50); } - }, 50); - } + }) + .catch((orientationError: any) => { + logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError); + }); - // Disable immersive mode + // Disable immersive mode (synchronous) disableImmersiveMode(); - // Navigate back to previous screen (StreamsScreen expected to be below Player) + // Navigate back IMMEDIATELY - don't wait for orientation try { if (navigation.canGoBack()) { navigation.goBack(); diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index c0d97c2a..d893569f 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -135,7 +135,7 @@ export const DEFAULT_SETTINGS: AppSettings = { showPosterTitles: true, enableHomeHeroBackground: true, // Trailer settings - showTrailers: true, // Enable trailers by default + showTrailers: false, // Trailers disabled by default trailerMuted: true, // Default to muted for better user experience // AI aiChatEnabled: false, diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index 0a585f5b..b4c76941 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -1,14 +1,14 @@ import { useState, useEffect, useCallback } from 'react'; import { AppState, AppStateStatus } from 'react-native'; -import { - traktService, - TraktUser, - TraktWatchedItem, +import { + traktService, + TraktUser, + TraktWatchedItem, TraktWatchlistItem, TraktCollectionItem, TraktRatingItem, - TraktContentData, - TraktPlaybackItem + TraktContentData, + TraktPlaybackItem } from '../services/traktService'; import { storageService } from '../services/storageService'; import { logger } from '../utils/logger'; @@ -25,8 +25,7 @@ export function useTraktIntegration() { const [collectionShows, setCollectionShows] = useState([]); const [continueWatching, setContinueWatching] = useState([]); const [ratedContent, setRatedContent] = useState([]); - const [lastAuthCheck, setLastAuthCheck] = useState(Date.now()); - + // State for real-time status tracking const [watchlistItems, setWatchlistItems] = useState>(new Set()); const [collectionItems, setCollectionItems] = useState>(new Set()); @@ -39,7 +38,7 @@ export function useTraktIntegration() { 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(); @@ -49,9 +48,8 @@ export function useTraktIntegration() { logger.log('[useTraktIntegration] User is not authenticated'); setUserProfile(null); } - - // Update the last auth check timestamp to trigger dependent components to update - setLastAuthCheck(Date.now()); + + } catch (error) { logger.error('[useTraktIntegration] Error checking auth status:', error); } finally { @@ -68,7 +66,7 @@ export function useTraktIntegration() { // Load watched items const loadWatchedItems = useCallback(async () => { if (!isAuthenticated) return; - + setIsLoading(true); try { const [movies, shows] = await Promise.all([ @@ -87,7 +85,7 @@ export function useTraktIntegration() { // Load all collections (watchlist, collection, continue watching, ratings) const loadAllCollections = useCallback(async () => { if (!isAuthenticated) return; - + setIsLoading(true); try { const [ @@ -105,44 +103,44 @@ export function useTraktIntegration() { traktService.getPlaybackProgressWithImages(), traktService.getRatingsWithImages() ]); - + setWatchlistMovies(watchlistMovies); setWatchlistShows(watchlistShows); setCollectionMovies(collectionMovies); setCollectionShows(collectionShows); setContinueWatching(continueWatching); setRatedContent(ratings); - + // Populate watchlist and collection sets for quick lookups const newWatchlistItems = new Set(); const newCollectionItems = new Set(); - + // Add movies to sets watchlistMovies.forEach(item => { if (item.movie?.ids?.imdb) { newWatchlistItems.add(`movie:${item.movie.ids.imdb}`); } }); - + collectionMovies.forEach(item => { if (item.movie?.ids?.imdb) { newCollectionItems.add(`movie:${item.movie.ids.imdb}`); } }); - + // Add shows to sets watchlistShows.forEach(item => { if (item.show?.ids?.imdb) { newWatchlistItems.add(`show:${item.show.ids.imdb}`); } }); - + collectionShows.forEach(item => { if (item.show?.ids?.imdb) { newCollectionItems.add(`show:${item.show.ids.imdb}`); } }); - + setWatchlistItems(newWatchlistItems); setCollectionItems(newCollectionItems); } catch (error) { @@ -155,7 +153,7 @@ export function useTraktIntegration() { // Check if a movie is watched const isMovieWatched = useCallback(async (imdbId: string): Promise => { if (!isAuthenticated) return false; - + try { return await traktService.isMovieWatched(imdbId); } catch (error) { @@ -166,12 +164,12 @@ export function useTraktIntegration() { // Check if an episode is watched const isEpisodeWatched = useCallback(async ( - imdbId: string, - season: number, + imdbId: string, + season: number, episode: number ): Promise => { if (!isAuthenticated) return false; - + try { return await traktService.isEpisodeWatched(imdbId, season, episode); } catch (error) { @@ -182,11 +180,11 @@ export function useTraktIntegration() { // Mark a movie as watched const markMovieAsWatched = useCallback(async ( - imdbId: string, + imdbId: string, watchedAt: Date = new Date() ): Promise => { if (!isAuthenticated) return false; - + try { const result = await traktService.addToWatchedMovies(imdbId, watchedAt); if (result) { @@ -203,7 +201,7 @@ export function useTraktIntegration() { // Add content to Trakt watchlist const addToWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { if (!isAuthenticated) return false; - + try { const success = await traktService.addToWatchlist(imdbId, type); if (success) { @@ -223,7 +221,7 @@ export function useTraktIntegration() { // Remove content from Trakt watchlist const removeFromWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { if (!isAuthenticated) return false; - + try { const success = await traktService.removeFromWatchlist(imdbId, type); if (success) { @@ -246,7 +244,7 @@ export function useTraktIntegration() { // Add content to Trakt collection const addToCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { if (!isAuthenticated) return false; - + try { const success = await traktService.addToCollection(imdbId, type); if (success) { @@ -265,7 +263,7 @@ export function useTraktIntegration() { // Remove content from Trakt collection const removeFromCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise => { if (!isAuthenticated) return false; - + try { const success = await traktService.removeFromCollection(imdbId, type); if (success) { @@ -301,13 +299,13 @@ export function useTraktIntegration() { // Mark an episode as watched const markEpisodeAsWatched = useCallback(async ( - imdbId: string, - season: number, - episode: number, + imdbId: string, + season: number, + episode: number, watchedAt: Date = new Date() ): Promise => { if (!isAuthenticated) return false; - + try { const result = await traktService.addToWatchedEpisodes(imdbId, season, episode, watchedAt); if (result) { @@ -324,7 +322,7 @@ export function useTraktIntegration() { // Start watching content (scrobble start) const startWatching = useCallback(async (contentData: TraktContentData, progress: number): Promise => { if (!isAuthenticated) return false; - + try { return await traktService.scrobbleStart(contentData, progress); } catch (error) { @@ -392,12 +390,12 @@ export function useTraktIntegration() { // Sync progress to Trakt (legacy method) const syncProgress = useCallback(async ( - contentData: TraktContentData, - progress: number, + contentData: TraktContentData, + progress: number, force: boolean = false ): Promise => { if (!isAuthenticated) return false; - + try { return await traktService.syncProgressToTrakt(contentData, progress, force); } catch (error) { @@ -409,12 +407,12 @@ export function useTraktIntegration() { // Get playback progress from Trakt const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise => { // getTraktPlaybackProgress call logging removed - + if (!isAuthenticated) { logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated'); return []; } - + try { // traktService.getPlaybackProgress call logging removed const result = await traktService.getPlaybackProgress(type); @@ -429,19 +427,19 @@ export function useTraktIntegration() { // Sync all local progress to Trakt const syncAllProgress = useCallback(async (): Promise => { if (!isAuthenticated) return false; - + try { const unsyncedProgress = await storageService.getUnsyncedProgress(); logger.log(`[useTraktIntegration] Found ${unsyncedProgress.length} unsynced progress entries`); - + let syncedCount = 0; const batchSize = 5; // Process in smaller batches const delayBetweenBatches = 2000; // 2 seconds between batches - + // Process items in batches to avoid overwhelming the API for (let i = 0; i < unsyncedProgress.length; i += batchSize) { const batch = unsyncedProgress.slice(i, i + batchSize); - + // Process batch items with individual error handling const batchPromises = batch.map(async (item) => { try { @@ -454,9 +452,9 @@ export function useTraktIntegration() { season: item.episodeId ? parseInt(item.episodeId.split('S')[1]?.split('E')[0] || '0') : undefined, episode: item.episodeId ? parseInt(item.episodeId.split('E')[1] || '0') : undefined }; - + const progressPercent = (item.progress.currentTime / item.progress.duration) * 100; - + const success = await traktService.syncProgressToTrakt(contentData, progressPercent, true); if (success) { await storageService.updateTraktSyncStatus(item.id, item.type, true, progressPercent, item.episodeId); @@ -468,17 +466,17 @@ export function useTraktIntegration() { return false; } }); - + // Wait for batch to complete const batchResults = await Promise.all(batchPromises); syncedCount += batchResults.filter(result => result).length; - + // Delay between batches to avoid rate limiting if (i + batchSize < unsyncedProgress.length) { await new Promise(resolve => setTimeout(resolve, delayBetweenBatches)); } } - + logger.log(`[useTraktIntegration] Synced ${syncedCount}/${unsyncedProgress.length} progress entries`); return syncedCount > 0; } catch (error) { @@ -492,26 +490,26 @@ export function useTraktIntegration() { if (!isAuthenticated) { return false; } - + try { // Fetch both playback progress and recently watched movies const [traktProgress, watchedMovies] = await Promise.all([ getTraktPlaybackProgress(), traktService.getWatchedMovies() ]); - + // Progress retrieval logging removed - + // Batch process all updates to reduce storage notifications const updatePromises: Promise[] = []; - + // Process playback progress (in-progress items) for (const item of traktProgress) { try { let id: string; let type: string; let episodeId: string | undefined; - + if (item.type === 'movie' && item.movie) { id = item.movie.ids.imdb; type = 'movie'; @@ -522,7 +520,7 @@ export function useTraktIntegration() { } else { continue; } - + // Try to calculate exact time if we have stored duration const exactTime = await (async () => { const storedDuration = await storageService.getContentDuration(id, type, episodeId); @@ -531,13 +529,13 @@ export function useTraktIntegration() { } return undefined; })(); - + updatePromises.push( storageService.mergeWithTraktProgress( - id, - type, - item.progress, - item.paused_at, + id, + type, + item.progress, + item.paused_at, episodeId, exactTime ) @@ -546,20 +544,20 @@ export function useTraktIntegration() { logger.error('[useTraktIntegration] Error preparing Trakt progress update:', error); } } - + // 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; - + updatePromises.push( storageService.mergeWithTraktProgress( - id, - 'movie', - 100, // 100% progress for watched items - watchedAt + id, + 'movie', + 100, // 100% progress for watched items + watchedAt ) ); } @@ -567,10 +565,10 @@ export function useTraktIntegration() { logger.error('[useTraktIntegration] Error preparing watched movie update:', error); } } - + // Execute all updates in parallel await Promise.all(updatePromises); - + // Trakt merge logging removed return true; } catch (error) { @@ -614,18 +612,15 @@ export function useTraktIntegration() { }; const subscription = AppState.addEventListener('change', handleAppStateChange); - + return () => { subscription?.remove(); }; }, [isAuthenticated, fetchAndMergeTraktProgress]); - // Trigger sync when auth status is manually refreshed (for login scenarios) - useEffect(() => { - if (isAuthenticated) { - fetchAndMergeTraktProgress(); - } - }, [lastAuthCheck, isAuthenticated, fetchAndMergeTraktProgress]); + // Note: Auth check sync removed - fetchAndMergeTraktProgress is already called + // by the isAuthenticated useEffect (lines 595-602) and app focus sync (lines 605-621) + // Having another useEffect on lastAuthCheck caused infinite update depth errors // Manual force sync function for testing/troubleshooting const forceSyncTraktProgress = useCallback(async (): Promise => { diff --git a/src/hooks/useWatchProgress.ts b/src/hooks/useWatchProgress.ts index 7e0f5a05..18eda986 100644 --- a/src/hooks/useWatchProgress.ts +++ b/src/hooks/useWatchProgress.ts @@ -14,14 +14,14 @@ interface WatchProgressData { } export const useWatchProgress = ( - id: string, - type: 'movie' | 'series', + id: string, + type: 'movie' | 'series', episodeId?: string, 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 => { // Try to parse from format "seriesId:season:episode" @@ -30,10 +30,10 @@ export const useWatchProgress = ( const [, seasonNum, episodeNum] = parts; // Find episode in our local episodes array const episode = episodes.find( - ep => ep.season_number === parseInt(seasonNum) && - ep.episode_number === parseInt(episodeNum) + ep => ep.season_number === parseInt(seasonNum) && + ep.episode_number === parseInt(episodeNum) ); - + if (episode) { return { seasonNumber: seasonNum, @@ -55,14 +55,14 @@ export const useWatchProgress = ( return null; }, [episodes]); - + // Enhanced load watch progress with Trakt integration const loadWatchProgress = useCallback(async () => { try { if (id && type) { if (type === 'series') { const allProgress = await storageService.getAllWatchProgress(); - + // Function to get episode number from episodeId const getEpisodeNumber = (epId: string) => { const parts = epId.split(':'); @@ -93,8 +93,8 @@ export const useWatchProgress = ( if (progress) { // Always show the current episode progress when viewing it specifically // This allows HeroSection to properly display watched state - setWatchProgress({ - ...progress, + setWatchProgress({ + ...progress, episodeId, traktSynced: progress.traktSynced, traktProgress: progress.traktProgress @@ -105,17 +105,17 @@ export const useWatchProgress = ( } else { // FIXED: Find the most recently watched episode instead of first unfinished // Sort by lastUpdated timestamp (most recent first) - const sortedProgresses = seriesProgresses.sort((a, b) => + const sortedProgresses = seriesProgresses.sort((a, b) => b.progress.lastUpdated - a.progress.lastUpdated ); - + if (sortedProgresses.length > 0) { // Use the most recently watched episode const mostRecentProgress = sortedProgresses[0]; const progress = mostRecentProgress.progress; - + // Removed excessive logging for most recent progress - + setWatchProgress({ ...progress, episodeId: mostRecentProgress.episodeId, @@ -133,8 +133,8 @@ export const useWatchProgress = ( if (progress && progress.currentTime > 0) { // Always show progress data, even if watched (≥95%) // The HeroSection will handle the "watched" state display - setWatchProgress({ - ...progress, + setWatchProgress({ + ...progress, episodeId, traktSynced: progress.traktSynced, traktProgress: progress.traktProgress @@ -167,19 +167,33 @@ export const useWatchProgress = ( return 'Resume'; }, [watchProgress]); - // Subscribe to storage changes for real-time updates + // Subscribe to storage changes for real-time updates (with debounce to prevent loops) useEffect(() => { + let debounceTimeout: NodeJS.Timeout | null = null; + const unsubscribe = storageService.subscribeToWatchProgressUpdates(() => { - loadWatchProgress(); + // Debounce rapid updates to prevent infinite loops + if (debounceTimeout) { + clearTimeout(debounceTimeout); + } + debounceTimeout = setTimeout(() => { + loadWatchProgress(); + }, 100); }); - - return unsubscribe; + + return () => { + if (debounceTimeout) { + clearTimeout(debounceTimeout); + } + unsubscribe(); + }; }, [loadWatchProgress]); - // Initial load + // Initial load - only once on mount useEffect(() => { loadWatchProgress(); - }, [loadWatchProgress]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id, type, episodeId]); // Only re-run when core IDs change, not when loadWatchProgress ref changes // Refresh when screen comes into focus useFocusEffect( @@ -188,15 +202,16 @@ export const useWatchProgress = ( }, [loadWatchProgress]) ); - // Re-load when Trakt authentication status changes + // Re-load when Trakt authentication status changes (with guard) useEffect(() => { - if (isTraktAuthenticated !== undefined) { - // Small delay to ensure Trakt context is fully initialized - setTimeout(() => { - loadWatchProgress(); - }, 100); - } - }, [isTraktAuthenticated, loadWatchProgress]); + // Skip on initial mount, only run when isTraktAuthenticated actually changes + const timeoutId = setTimeout(() => { + loadWatchProgress(); + }, 200); // Slightly longer delay to avoid race conditions + + return () => clearTimeout(timeoutId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isTraktAuthenticated]); // Intentionally exclude loadWatchProgress to prevent loops return { watchProgress, diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 142e53a7..f750139d 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -1174,8 +1174,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta component={MetadataScreen} options={{ headerShown: false, - animation: Platform.OS === 'android' ? 'none' : 'fade', - animationDuration: Platform.OS === 'android' ? 0 : 300, + animation: Platform.OS === 'android' ? 'fade' : 'fade', + animationDuration: Platform.OS === 'android' ? 200 : 300, ...(Platform.OS === 'ios' && { cardStyleInterpolator: customFadeInterpolator, animationTypeForReplace: 'push', @@ -1192,8 +1192,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta component={StreamsScreen as any} options={{ headerShown: false, - animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none', - animationDuration: Platform.OS === 'android' ? 0 : 300, + animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'fade', + animationDuration: Platform.OS === 'android' ? 200 : 300, gestureEnabled: true, gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal', ...(Platform.OS === 'ios' && { presentation: 'modal' }), @@ -1542,8 +1542,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta name="AIChat" component={AIChatScreen} options={{ - animation: Platform.OS === 'android' ? 'none' : 'slide_from_right', - animationDuration: Platform.OS === 'android' ? 220 : 300, + animation: Platform.OS === 'android' ? 'fade' : 'slide_from_right', + animationDuration: Platform.OS === 'android' ? 200 : 300, presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'modal', gestureEnabled: true, gestureDirection: Platform.OS === 'ios' ? 'horizontal' : 'vertical',