bug fixes android and ios

This commit is contained in:
tapframe 2025-12-15 01:36:10 +05:30
parent 181cdaecb5
commit dbbee06a55
6 changed files with 228 additions and 214 deletions

View file

@ -861,18 +861,21 @@ const AndroidVideoPlayer: React.FC = () => {
// Re-apply immersive mode on layout changes to keep system bars hidden // Re-apply immersive mode on layout changes to keep system bars hidden
enableImmersiveMode(); enableImmersiveMode();
}); });
const initializePlayer = async () => {
StatusBar.setHidden(true, 'none');
enableImmersiveMode();
startOpeningAnimation();
// Initialize current volume and brightness levels // Immediate player setup - UI critical
// Volume starts at 1.0 (full volume) - React Native Video handles this natively StatusBar.setHidden(true, 'none');
setVolume(1.0); enableImmersiveMode();
if (DEBUG_MODE) { startOpeningAnimation();
logger.log(`[AndroidVideoPlayer] Initial volume: 1.0 (native)`);
}
// 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 { try {
// Capture Android system brightness and mode to restore later // Capture Android system brightness and mode to restore later
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
@ -900,10 +903,11 @@ const AndroidVideoPlayer: React.FC = () => {
// Fallback to 1.0 if brightness API fails // Fallback to 1.0 if brightness API fails
setBrightness(1.0); setBrightness(1.0);
} }
}; });
initializePlayer();
return () => { return () => {
subscription?.remove(); subscription?.remove();
brightnessTask.cancel();
disableImmersiveMode(); 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 // Disable immersive mode immediately (synchronous)
ScreenOrientation.unlockAsync().then(() => { disableImmersiveMode();
// 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();
// Simple back navigation (StreamsScreen should be below Player) // Navigate IMMEDIATELY - don't wait for orientation changes
if ((navigation as any).canGoBack && (navigation as any).canGoBack()) { if ((navigation as any).canGoBack && (navigation as any).canGoBack()) {
(navigation as any).goBack(); (navigation as any).goBack();
} else { } else {
// Fallback to Streams if stack isn't present // Fallback to Streams if stack isn't present
(navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true }); (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();
// Simple back navigation fallback path // Fire orientation changes in background - don't await
if ((navigation as any).canGoBack && (navigation as any).canGoBack()) { ScreenOrientation.unlockAsync()
(navigation as any).goBack(); .then(() => {
} else { // On tablets keep rotation unlocked; on phones, return to portrait
(navigation as any).navigate('Streams', { id, type, episodeId, fromPlayer: true }); 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) // Send Trakt sync in background (don't await)
const backgroundSync = async () => { const backgroundSync = async () => {

View file

@ -589,20 +589,19 @@ const KSPlayerCore: React.FC = () => {
// Force landscape orientation after opening animation completes // Force landscape orientation after opening animation completes
useEffect(() => { useEffect(() => {
const lockOrientation = async () => { // Defer orientation lock until after navigation animation to prevent sluggishness
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
if (isOpeningAnimationComplete) { 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 () => { return () => {
// Do not unlock orientation here; we unlock explicitly on close to avoid mid-transition flips // Do not unlock orientation here; we unlock explicitly on close to avoid mid-transition flips
}; };
@ -616,21 +615,24 @@ const KSPlayerCore: React.FC = () => {
enableImmersiveMode(); 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 // Immediate player setup - UI critical
// Volume starts at 100 (full volume) for KSPlayer StatusBar.setHidden(true, 'none');
setVolume(100); // Enable immersive mode after opening animation to prevent glitches
if (DEBUG_MODE) { if (isOpeningAnimationComplete) {
logger.log(`[VideoPlayer] Initial volume: 100 (KSPlayer native)`); 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 { try {
const currentBrightness = await Brightness.getBrightnessAsync(); const currentBrightness = await Brightness.getBrightnessAsync();
setBrightness(currentBrightness); setBrightness(currentBrightness);
@ -642,10 +644,11 @@ const KSPlayerCore: React.FC = () => {
// Fallback to 1.0 if brightness API fails // Fallback to 1.0 if brightness API fails
setBrightness(1.0); setBrightness(1.0);
} }
}; });
initializePlayer();
return () => { return () => {
subscription?.remove(); subscription?.remove();
brightnessTask.cancel();
disableImmersiveMode(); disableImmersiveMode();
}; };
}, [isOpeningAnimationComplete]); }, [isOpeningAnimationComplete]);
@ -1381,32 +1384,32 @@ const KSPlayerCore: React.FC = () => {
logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`); logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
// Cleanup and navigate back immediately without delay // Cleanup and navigate back immediately without delay
const cleanup = async () => { const cleanup = () => {
try { // Fire orientation changes in background - don't await them
// Unlock orientation first ScreenOrientation.unlockAsync()
await ScreenOrientation.unlockAsync(); .then(() => {
logger.log('[VideoPlayer] Orientation unlocked'); logger.log('[VideoPlayer] Orientation unlocked');
} catch (orientationError) { // On iOS tablets, keep rotation unlocked; on phones, return to portrait
logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError); if (Platform.OS === 'ios') {
} const { width: dw, height: dh } = Dimensions.get('window');
const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768;
// On iOS tablets, keep rotation unlocked; on phones, return to portrait setTimeout(() => {
if (Platform.OS === 'ios') { if (isTablet) {
const { width: dw, height: dh } = Dimensions.get('window'); ScreenOrientation.unlockAsync().catch(() => { });
const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768; } else {
setTimeout(() => { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { });
if (isTablet) { }
ScreenOrientation.unlockAsync().catch(() => { }); }, 50);
} else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { });
} }
}, 50); })
} .catch((orientationError: any) => {
logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError);
});
// Disable immersive mode // Disable immersive mode (synchronous)
disableImmersiveMode(); disableImmersiveMode();
// Navigate back to previous screen (StreamsScreen expected to be below Player) // Navigate back IMMEDIATELY - don't wait for orientation
try { try {
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
navigation.goBack(); navigation.goBack();

View file

@ -135,7 +135,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
showPosterTitles: true, showPosterTitles: true,
enableHomeHeroBackground: true, enableHomeHeroBackground: true,
// Trailer settings // Trailer settings
showTrailers: true, // Enable trailers by default showTrailers: false, // Trailers disabled by default
trailerMuted: true, // Default to muted for better user experience trailerMuted: true, // Default to muted for better user experience
// AI // AI
aiChatEnabled: false, aiChatEnabled: false,

View file

@ -1,14 +1,14 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { AppState, AppStateStatus } from 'react-native'; import { AppState, AppStateStatus } from 'react-native';
import { import {
traktService, traktService,
TraktUser, TraktUser,
TraktWatchedItem, TraktWatchedItem,
TraktWatchlistItem, TraktWatchlistItem,
TraktCollectionItem, TraktCollectionItem,
TraktRatingItem, TraktRatingItem,
TraktContentData, TraktContentData,
TraktPlaybackItem TraktPlaybackItem
} from '../services/traktService'; } from '../services/traktService';
import { storageService } from '../services/storageService'; import { storageService } from '../services/storageService';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
@ -25,8 +25,7 @@ export function useTraktIntegration() {
const [collectionShows, setCollectionShows] = useState<TraktCollectionItem[]>([]); const [collectionShows, setCollectionShows] = useState<TraktCollectionItem[]>([]);
const [continueWatching, setContinueWatching] = useState<TraktPlaybackItem[]>([]); const [continueWatching, setContinueWatching] = useState<TraktPlaybackItem[]>([]);
const [ratedContent, setRatedContent] = useState<TraktRatingItem[]>([]); const [ratedContent, setRatedContent] = useState<TraktRatingItem[]>([]);
const [lastAuthCheck, setLastAuthCheck] = useState<number>(Date.now());
// State for real-time status tracking // State for real-time status tracking
const [watchlistItems, setWatchlistItems] = useState<Set<string>>(new Set()); const [watchlistItems, setWatchlistItems] = useState<Set<string>>(new Set());
const [collectionItems, setCollectionItems] = useState<Set<string>>(new Set()); const [collectionItems, setCollectionItems] = useState<Set<string>>(new Set());
@ -39,7 +38,7 @@ export function useTraktIntegration() {
const authenticated = await traktService.isAuthenticated(); const authenticated = await traktService.isAuthenticated();
logger.log(`[useTraktIntegration] Authentication check result: ${authenticated}`); logger.log(`[useTraktIntegration] Authentication check result: ${authenticated}`);
setIsAuthenticated(authenticated); setIsAuthenticated(authenticated);
if (authenticated) { if (authenticated) {
logger.log('[useTraktIntegration] User is authenticated, fetching profile...'); logger.log('[useTraktIntegration] User is authenticated, fetching profile...');
const profile = await traktService.getUserProfile(); const profile = await traktService.getUserProfile();
@ -49,9 +48,8 @@ export function useTraktIntegration() {
logger.log('[useTraktIntegration] User is not authenticated'); logger.log('[useTraktIntegration] User is not authenticated');
setUserProfile(null); setUserProfile(null);
} }
// Update the last auth check timestamp to trigger dependent components to update
setLastAuthCheck(Date.now());
} catch (error) { } catch (error) {
logger.error('[useTraktIntegration] Error checking auth status:', error); logger.error('[useTraktIntegration] Error checking auth status:', error);
} finally { } finally {
@ -68,7 +66,7 @@ export function useTraktIntegration() {
// Load watched items // Load watched items
const loadWatchedItems = useCallback(async () => { const loadWatchedItems = useCallback(async () => {
if (!isAuthenticated) return; if (!isAuthenticated) return;
setIsLoading(true); setIsLoading(true);
try { try {
const [movies, shows] = await Promise.all([ const [movies, shows] = await Promise.all([
@ -87,7 +85,7 @@ export function useTraktIntegration() {
// Load all collections (watchlist, collection, continue watching, ratings) // Load all collections (watchlist, collection, continue watching, ratings)
const loadAllCollections = useCallback(async () => { const loadAllCollections = useCallback(async () => {
if (!isAuthenticated) return; if (!isAuthenticated) return;
setIsLoading(true); setIsLoading(true);
try { try {
const [ const [
@ -105,44 +103,44 @@ export function useTraktIntegration() {
traktService.getPlaybackProgressWithImages(), traktService.getPlaybackProgressWithImages(),
traktService.getRatingsWithImages() traktService.getRatingsWithImages()
]); ]);
setWatchlistMovies(watchlistMovies); setWatchlistMovies(watchlistMovies);
setWatchlistShows(watchlistShows); setWatchlistShows(watchlistShows);
setCollectionMovies(collectionMovies); setCollectionMovies(collectionMovies);
setCollectionShows(collectionShows); setCollectionShows(collectionShows);
setContinueWatching(continueWatching); setContinueWatching(continueWatching);
setRatedContent(ratings); setRatedContent(ratings);
// Populate watchlist and collection sets for quick lookups // Populate watchlist and collection sets for quick lookups
const newWatchlistItems = new Set<string>(); const newWatchlistItems = new Set<string>();
const newCollectionItems = new Set<string>(); const newCollectionItems = new Set<string>();
// Add movies to sets // Add movies to sets
watchlistMovies.forEach(item => { watchlistMovies.forEach(item => {
if (item.movie?.ids?.imdb) { if (item.movie?.ids?.imdb) {
newWatchlistItems.add(`movie:${item.movie.ids.imdb}`); newWatchlistItems.add(`movie:${item.movie.ids.imdb}`);
} }
}); });
collectionMovies.forEach(item => { collectionMovies.forEach(item => {
if (item.movie?.ids?.imdb) { if (item.movie?.ids?.imdb) {
newCollectionItems.add(`movie:${item.movie.ids.imdb}`); newCollectionItems.add(`movie:${item.movie.ids.imdb}`);
} }
}); });
// Add shows to sets // Add shows to sets
watchlistShows.forEach(item => { watchlistShows.forEach(item => {
if (item.show?.ids?.imdb) { if (item.show?.ids?.imdb) {
newWatchlistItems.add(`show:${item.show.ids.imdb}`); newWatchlistItems.add(`show:${item.show.ids.imdb}`);
} }
}); });
collectionShows.forEach(item => { collectionShows.forEach(item => {
if (item.show?.ids?.imdb) { if (item.show?.ids?.imdb) {
newCollectionItems.add(`show:${item.show.ids.imdb}`); newCollectionItems.add(`show:${item.show.ids.imdb}`);
} }
}); });
setWatchlistItems(newWatchlistItems); setWatchlistItems(newWatchlistItems);
setCollectionItems(newCollectionItems); setCollectionItems(newCollectionItems);
} catch (error) { } catch (error) {
@ -155,7 +153,7 @@ export function useTraktIntegration() {
// Check if a movie is watched // Check if a movie is watched
const isMovieWatched = useCallback(async (imdbId: string): Promise<boolean> => { const isMovieWatched = useCallback(async (imdbId: string): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
return await traktService.isMovieWatched(imdbId); return await traktService.isMovieWatched(imdbId);
} catch (error) { } catch (error) {
@ -166,12 +164,12 @@ export function useTraktIntegration() {
// Check if an episode is watched // Check if an episode is watched
const isEpisodeWatched = useCallback(async ( const isEpisodeWatched = useCallback(async (
imdbId: string, imdbId: string,
season: number, season: number,
episode: number episode: number
): Promise<boolean> => { ): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
return await traktService.isEpisodeWatched(imdbId, season, episode); return await traktService.isEpisodeWatched(imdbId, season, episode);
} catch (error) { } catch (error) {
@ -182,11 +180,11 @@ export function useTraktIntegration() {
// Mark a movie as watched // Mark a movie as watched
const markMovieAsWatched = useCallback(async ( const markMovieAsWatched = useCallback(async (
imdbId: string, imdbId: string,
watchedAt: Date = new Date() watchedAt: Date = new Date()
): Promise<boolean> => { ): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const result = await traktService.addToWatchedMovies(imdbId, watchedAt); const result = await traktService.addToWatchedMovies(imdbId, watchedAt);
if (result) { if (result) {
@ -203,7 +201,7 @@ export function useTraktIntegration() {
// Add content to Trakt watchlist // Add content to Trakt watchlist
const addToWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => { const addToWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const success = await traktService.addToWatchlist(imdbId, type); const success = await traktService.addToWatchlist(imdbId, type);
if (success) { if (success) {
@ -223,7 +221,7 @@ export function useTraktIntegration() {
// Remove content from Trakt watchlist // Remove content from Trakt watchlist
const removeFromWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => { const removeFromWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const success = await traktService.removeFromWatchlist(imdbId, type); const success = await traktService.removeFromWatchlist(imdbId, type);
if (success) { if (success) {
@ -246,7 +244,7 @@ export function useTraktIntegration() {
// Add content to Trakt collection // Add content to Trakt collection
const addToCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => { const addToCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const success = await traktService.addToCollection(imdbId, type); const success = await traktService.addToCollection(imdbId, type);
if (success) { if (success) {
@ -265,7 +263,7 @@ export function useTraktIntegration() {
// Remove content from Trakt collection // Remove content from Trakt collection
const removeFromCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => { const removeFromCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const success = await traktService.removeFromCollection(imdbId, type); const success = await traktService.removeFromCollection(imdbId, type);
if (success) { if (success) {
@ -301,13 +299,13 @@ export function useTraktIntegration() {
// Mark an episode as watched // Mark an episode as watched
const markEpisodeAsWatched = useCallback(async ( const markEpisodeAsWatched = useCallback(async (
imdbId: string, imdbId: string,
season: number, season: number,
episode: number, episode: number,
watchedAt: Date = new Date() watchedAt: Date = new Date()
): Promise<boolean> => { ): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const result = await traktService.addToWatchedEpisodes(imdbId, season, episode, watchedAt); const result = await traktService.addToWatchedEpisodes(imdbId, season, episode, watchedAt);
if (result) { if (result) {
@ -324,7 +322,7 @@ export function useTraktIntegration() {
// Start watching content (scrobble start) // Start watching content (scrobble start)
const startWatching = useCallback(async (contentData: TraktContentData, progress: number): Promise<boolean> => { const startWatching = useCallback(async (contentData: TraktContentData, progress: number): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
return await traktService.scrobbleStart(contentData, progress); return await traktService.scrobbleStart(contentData, progress);
} catch (error) { } catch (error) {
@ -392,12 +390,12 @@ export function useTraktIntegration() {
// Sync progress to Trakt (legacy method) // Sync progress to Trakt (legacy method)
const syncProgress = useCallback(async ( const syncProgress = useCallback(async (
contentData: TraktContentData, contentData: TraktContentData,
progress: number, progress: number,
force: boolean = false force: boolean = false
): Promise<boolean> => { ): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
return await traktService.syncProgressToTrakt(contentData, progress, force); return await traktService.syncProgressToTrakt(contentData, progress, force);
} catch (error) { } catch (error) {
@ -409,12 +407,12 @@ export function useTraktIntegration() {
// Get playback progress from Trakt // Get playback progress from Trakt
const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise<TraktPlaybackItem[]> => { const getTraktPlaybackProgress = useCallback(async (type?: 'movies' | 'shows'): Promise<TraktPlaybackItem[]> => {
// getTraktPlaybackProgress call logging removed // getTraktPlaybackProgress call logging removed
if (!isAuthenticated) { if (!isAuthenticated) {
logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated'); logger.log('[useTraktIntegration] getTraktPlaybackProgress: Not authenticated');
return []; return [];
} }
try { try {
// traktService.getPlaybackProgress call logging removed // traktService.getPlaybackProgress call logging removed
const result = await traktService.getPlaybackProgress(type); const result = await traktService.getPlaybackProgress(type);
@ -429,19 +427,19 @@ export function useTraktIntegration() {
// Sync all local progress to Trakt // Sync all local progress to Trakt
const syncAllProgress = useCallback(async (): Promise<boolean> => { const syncAllProgress = useCallback(async (): Promise<boolean> => {
if (!isAuthenticated) return false; if (!isAuthenticated) return false;
try { try {
const unsyncedProgress = await storageService.getUnsyncedProgress(); const unsyncedProgress = await storageService.getUnsyncedProgress();
logger.log(`[useTraktIntegration] Found ${unsyncedProgress.length} unsynced progress entries`); logger.log(`[useTraktIntegration] Found ${unsyncedProgress.length} unsynced progress entries`);
let syncedCount = 0; let syncedCount = 0;
const batchSize = 5; // Process in smaller batches const batchSize = 5; // Process in smaller batches
const delayBetweenBatches = 2000; // 2 seconds between batches const delayBetweenBatches = 2000; // 2 seconds between batches
// Process items in batches to avoid overwhelming the API // Process items in batches to avoid overwhelming the API
for (let i = 0; i < unsyncedProgress.length; i += batchSize) { for (let i = 0; i < unsyncedProgress.length; i += batchSize) {
const batch = unsyncedProgress.slice(i, i + batchSize); const batch = unsyncedProgress.slice(i, i + batchSize);
// Process batch items with individual error handling // Process batch items with individual error handling
const batchPromises = batch.map(async (item) => { const batchPromises = batch.map(async (item) => {
try { try {
@ -454,9 +452,9 @@ export function useTraktIntegration() {
season: item.episodeId ? parseInt(item.episodeId.split('S')[1]?.split('E')[0] || '0') : undefined, 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 episode: item.episodeId ? parseInt(item.episodeId.split('E')[1] || '0') : undefined
}; };
const progressPercent = (item.progress.currentTime / item.progress.duration) * 100; const progressPercent = (item.progress.currentTime / item.progress.duration) * 100;
const success = await traktService.syncProgressToTrakt(contentData, progressPercent, true); const success = await traktService.syncProgressToTrakt(contentData, progressPercent, true);
if (success) { if (success) {
await storageService.updateTraktSyncStatus(item.id, item.type, true, progressPercent, item.episodeId); await storageService.updateTraktSyncStatus(item.id, item.type, true, progressPercent, item.episodeId);
@ -468,17 +466,17 @@ export function useTraktIntegration() {
return false; return false;
} }
}); });
// Wait for batch to complete // Wait for batch to complete
const batchResults = await Promise.all(batchPromises); const batchResults = await Promise.all(batchPromises);
syncedCount += batchResults.filter(result => result).length; syncedCount += batchResults.filter(result => result).length;
// Delay between batches to avoid rate limiting // Delay between batches to avoid rate limiting
if (i + batchSize < unsyncedProgress.length) { if (i + batchSize < unsyncedProgress.length) {
await new Promise(resolve => setTimeout(resolve, delayBetweenBatches)); await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
} }
} }
logger.log(`[useTraktIntegration] Synced ${syncedCount}/${unsyncedProgress.length} progress entries`); logger.log(`[useTraktIntegration] Synced ${syncedCount}/${unsyncedProgress.length} progress entries`);
return syncedCount > 0; return syncedCount > 0;
} catch (error) { } catch (error) {
@ -492,26 +490,26 @@ export function useTraktIntegration() {
if (!isAuthenticated) { if (!isAuthenticated) {
return false; return false;
} }
try { try {
// Fetch both playback progress and recently watched movies // Fetch both playback progress and recently watched movies
const [traktProgress, watchedMovies] = await Promise.all([ const [traktProgress, watchedMovies] = await Promise.all([
getTraktPlaybackProgress(), getTraktPlaybackProgress(),
traktService.getWatchedMovies() traktService.getWatchedMovies()
]); ]);
// Progress retrieval logging removed // Progress retrieval logging removed
// Batch process all updates to reduce storage notifications // Batch process all updates to reduce storage notifications
const updatePromises: Promise<void>[] = []; const updatePromises: Promise<void>[] = [];
// Process playback progress (in-progress items) // Process playback progress (in-progress items)
for (const item of traktProgress) { for (const item of traktProgress) {
try { try {
let id: string; let id: string;
let type: string; let type: string;
let episodeId: string | undefined; let episodeId: string | undefined;
if (item.type === 'movie' && item.movie) { if (item.type === 'movie' && item.movie) {
id = item.movie.ids.imdb; id = item.movie.ids.imdb;
type = 'movie'; type = 'movie';
@ -522,7 +520,7 @@ export function useTraktIntegration() {
} else { } else {
continue; continue;
} }
// Try to calculate exact time if we have stored duration // Try to calculate exact time if we have stored duration
const exactTime = await (async () => { const exactTime = await (async () => {
const storedDuration = await storageService.getContentDuration(id, type, episodeId); const storedDuration = await storageService.getContentDuration(id, type, episodeId);
@ -531,13 +529,13 @@ export function useTraktIntegration() {
} }
return undefined; return undefined;
})(); })();
updatePromises.push( updatePromises.push(
storageService.mergeWithTraktProgress( storageService.mergeWithTraktProgress(
id, id,
type, type,
item.progress, item.progress,
item.paused_at, item.paused_at,
episodeId, episodeId,
exactTime exactTime
) )
@ -546,20 +544,20 @@ export function useTraktIntegration() {
logger.error('[useTraktIntegration] Error preparing Trakt progress update:', error); logger.error('[useTraktIntegration] Error preparing Trakt progress update:', error);
} }
} }
// Process watched movies (100% completed) // Process watched movies (100% completed)
for (const movie of watchedMovies) { for (const movie of watchedMovies) {
try { try {
if (movie.movie?.ids?.imdb) { if (movie.movie?.ids?.imdb) {
const id = movie.movie.ids.imdb; const id = movie.movie.ids.imdb;
const watchedAt = movie.last_watched_at; const watchedAt = movie.last_watched_at;
updatePromises.push( updatePromises.push(
storageService.mergeWithTraktProgress( storageService.mergeWithTraktProgress(
id, id,
'movie', 'movie',
100, // 100% progress for watched items 100, // 100% progress for watched items
watchedAt watchedAt
) )
); );
} }
@ -567,10 +565,10 @@ export function useTraktIntegration() {
logger.error('[useTraktIntegration] Error preparing watched movie update:', error); logger.error('[useTraktIntegration] Error preparing watched movie update:', error);
} }
} }
// Execute all updates in parallel // Execute all updates in parallel
await Promise.all(updatePromises); await Promise.all(updatePromises);
// Trakt merge logging removed // Trakt merge logging removed
return true; return true;
} catch (error) { } catch (error) {
@ -614,18 +612,15 @@ export function useTraktIntegration() {
}; };
const subscription = AppState.addEventListener('change', handleAppStateChange); const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => { return () => {
subscription?.remove(); subscription?.remove();
}; };
}, [isAuthenticated, fetchAndMergeTraktProgress]); }, [isAuthenticated, fetchAndMergeTraktProgress]);
// Trigger sync when auth status is manually refreshed (for login scenarios) // Note: Auth check sync removed - fetchAndMergeTraktProgress is already called
useEffect(() => { // by the isAuthenticated useEffect (lines 595-602) and app focus sync (lines 605-621)
if (isAuthenticated) { // Having another useEffect on lastAuthCheck caused infinite update depth errors
fetchAndMergeTraktProgress();
}
}, [lastAuthCheck, isAuthenticated, fetchAndMergeTraktProgress]);
// Manual force sync function for testing/troubleshooting // Manual force sync function for testing/troubleshooting
const forceSyncTraktProgress = useCallback(async (): Promise<boolean> => { const forceSyncTraktProgress = useCallback(async (): Promise<boolean> => {

View file

@ -14,14 +14,14 @@ interface WatchProgressData {
} }
export const useWatchProgress = ( export const useWatchProgress = (
id: string, id: string,
type: 'movie' | 'series', type: 'movie' | 'series',
episodeId?: string, episodeId?: string,
episodes: any[] = [] episodes: any[] = []
) => { ) => {
const [watchProgress, setWatchProgress] = useState<WatchProgressData | null>(null); const [watchProgress, setWatchProgress] = useState<WatchProgressData | null>(null);
const { isAuthenticated: isTraktAuthenticated } = useTraktContext(); const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
// Function to get episode details from episodeId // Function to get episode details from episodeId
const getEpisodeDetails = useCallback((episodeId: string): { seasonNumber: string; episodeNumber: string; episodeName: string } | null => { const getEpisodeDetails = useCallback((episodeId: string): { seasonNumber: string; episodeNumber: string; episodeName: string } | null => {
// Try to parse from format "seriesId:season:episode" // Try to parse from format "seriesId:season:episode"
@ -30,10 +30,10 @@ export const useWatchProgress = (
const [, seasonNum, episodeNum] = parts; const [, seasonNum, episodeNum] = parts;
// Find episode in our local episodes array // Find episode in our local episodes array
const episode = episodes.find( const episode = episodes.find(
ep => ep.season_number === parseInt(seasonNum) && ep => ep.season_number === parseInt(seasonNum) &&
ep.episode_number === parseInt(episodeNum) ep.episode_number === parseInt(episodeNum)
); );
if (episode) { if (episode) {
return { return {
seasonNumber: seasonNum, seasonNumber: seasonNum,
@ -55,14 +55,14 @@ export const useWatchProgress = (
return null; return null;
}, [episodes]); }, [episodes]);
// Enhanced load watch progress with Trakt integration // Enhanced load watch progress with Trakt integration
const loadWatchProgress = useCallback(async () => { const loadWatchProgress = useCallback(async () => {
try { try {
if (id && type) { if (id && type) {
if (type === 'series') { if (type === 'series') {
const allProgress = await storageService.getAllWatchProgress(); const allProgress = await storageService.getAllWatchProgress();
// Function to get episode number from episodeId // Function to get episode number from episodeId
const getEpisodeNumber = (epId: string) => { const getEpisodeNumber = (epId: string) => {
const parts = epId.split(':'); const parts = epId.split(':');
@ -93,8 +93,8 @@ export const useWatchProgress = (
if (progress) { if (progress) {
// Always show the current episode progress when viewing it specifically // Always show the current episode progress when viewing it specifically
// This allows HeroSection to properly display watched state // This allows HeroSection to properly display watched state
setWatchProgress({ setWatchProgress({
...progress, ...progress,
episodeId, episodeId,
traktSynced: progress.traktSynced, traktSynced: progress.traktSynced,
traktProgress: progress.traktProgress traktProgress: progress.traktProgress
@ -105,17 +105,17 @@ export const useWatchProgress = (
} else { } else {
// FIXED: Find the most recently watched episode instead of first unfinished // FIXED: Find the most recently watched episode instead of first unfinished
// Sort by lastUpdated timestamp (most recent first) // 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 b.progress.lastUpdated - a.progress.lastUpdated
); );
if (sortedProgresses.length > 0) { if (sortedProgresses.length > 0) {
// Use the most recently watched episode // Use the most recently watched episode
const mostRecentProgress = sortedProgresses[0]; const mostRecentProgress = sortedProgresses[0];
const progress = mostRecentProgress.progress; const progress = mostRecentProgress.progress;
// Removed excessive logging for most recent progress // Removed excessive logging for most recent progress
setWatchProgress({ setWatchProgress({
...progress, ...progress,
episodeId: mostRecentProgress.episodeId, episodeId: mostRecentProgress.episodeId,
@ -133,8 +133,8 @@ export const useWatchProgress = (
if (progress && progress.currentTime > 0) { if (progress && progress.currentTime > 0) {
// Always show progress data, even if watched (≥95%) // Always show progress data, even if watched (≥95%)
// The HeroSection will handle the "watched" state display // The HeroSection will handle the "watched" state display
setWatchProgress({ setWatchProgress({
...progress, ...progress,
episodeId, episodeId,
traktSynced: progress.traktSynced, traktSynced: progress.traktSynced,
traktProgress: progress.traktProgress traktProgress: progress.traktProgress
@ -167,19 +167,33 @@ export const useWatchProgress = (
return 'Resume'; return 'Resume';
}, [watchProgress]); }, [watchProgress]);
// Subscribe to storage changes for real-time updates // Subscribe to storage changes for real-time updates (with debounce to prevent loops)
useEffect(() => { useEffect(() => {
let debounceTimeout: NodeJS.Timeout | null = null;
const unsubscribe = storageService.subscribeToWatchProgressUpdates(() => { 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]); }, [loadWatchProgress]);
// Initial load // Initial load - only once on mount
useEffect(() => { useEffect(() => {
loadWatchProgress(); 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 // Refresh when screen comes into focus
useFocusEffect( useFocusEffect(
@ -188,15 +202,16 @@ export const useWatchProgress = (
}, [loadWatchProgress]) }, [loadWatchProgress])
); );
// Re-load when Trakt authentication status changes // Re-load when Trakt authentication status changes (with guard)
useEffect(() => { useEffect(() => {
if (isTraktAuthenticated !== undefined) { // Skip on initial mount, only run when isTraktAuthenticated actually changes
// Small delay to ensure Trakt context is fully initialized const timeoutId = setTimeout(() => {
setTimeout(() => { loadWatchProgress();
loadWatchProgress(); }, 200); // Slightly longer delay to avoid race conditions
}, 100);
} return () => clearTimeout(timeoutId);
}, [isTraktAuthenticated, loadWatchProgress]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTraktAuthenticated]); // Intentionally exclude loadWatchProgress to prevent loops
return { return {
watchProgress, watchProgress,

View file

@ -1174,8 +1174,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
component={MetadataScreen} component={MetadataScreen}
options={{ options={{
headerShown: false, headerShown: false,
animation: Platform.OS === 'android' ? 'none' : 'fade', animation: Platform.OS === 'android' ? 'fade' : 'fade',
animationDuration: Platform.OS === 'android' ? 0 : 300, animationDuration: Platform.OS === 'android' ? 200 : 300,
...(Platform.OS === 'ios' && { ...(Platform.OS === 'ios' && {
cardStyleInterpolator: customFadeInterpolator, cardStyleInterpolator: customFadeInterpolator,
animationTypeForReplace: 'push', animationTypeForReplace: 'push',
@ -1192,8 +1192,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
component={StreamsScreen as any} component={StreamsScreen as any}
options={{ options={{
headerShown: false, headerShown: false,
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none', animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'fade',
animationDuration: Platform.OS === 'android' ? 0 : 300, animationDuration: Platform.OS === 'android' ? 200 : 300,
gestureEnabled: true, gestureEnabled: true,
gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal', gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal',
...(Platform.OS === 'ios' && { presentation: 'modal' }), ...(Platform.OS === 'ios' && { presentation: 'modal' }),
@ -1542,8 +1542,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
name="AIChat" name="AIChat"
component={AIChatScreen} component={AIChatScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'none' : 'slide_from_right', animation: Platform.OS === 'android' ? 'fade' : 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 220 : 300, animationDuration: Platform.OS === 'android' ? 200 : 300,
presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'modal', presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'modal',
gestureEnabled: true, gestureEnabled: true,
gestureDirection: Platform.OS === 'ios' ? 'horizontal' : 'vertical', gestureDirection: Platform.OS === 'ios' ? 'horizontal' : 'vertical',