mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
bug fixes android and ios
This commit is contained in:
parent
181cdaecb5
commit
dbbee06a55
6 changed files with 228 additions and 214 deletions
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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> => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue