diff --git a/libmpv-android b/libmpv-android
index db3b10e6..1a94da42 160000
--- a/libmpv-android
+++ b/libmpv-android
@@ -1 +1 @@
-Subproject commit db3b10e64353349d0d72619ca7d779829e36fe4d
+Subproject commit 1a94da42094b524b94a28902ae43c27e3286460d
diff --git a/mpv-android b/mpv-android
index 118cd1ed..a31e9a0d 160000
--- a/mpv-android
+++ b/mpv-android
@@ -1 +1 @@
-Subproject commit 118cd1ed3d498265e44230e5dbb015bdd59f9dad
+Subproject commit a31e9a0d270066deb41fe330ed34ddeb0e38f0ab
diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx
index e720f745..73b586cc 100644
--- a/src/components/metadata/HeroSection.tsx
+++ b/src/components/metadata/HeroSection.tsx
@@ -107,13 +107,13 @@ interface HeroSectionProps {
}
// Ultra-optimized ActionButtons Component - minimal re-renders
-const ActionButtons = memo(({
- handleShowStreams,
- toggleLibrary,
- inLibrary,
- type,
- id,
- navigation,
+const ActionButtons = memo(({
+ handleShowStreams,
+ toggleLibrary,
+ inLibrary,
+ type,
+ id,
+ navigation,
playButtonText,
animatedStyle,
isWatched,
@@ -150,21 +150,21 @@ const ActionButtons = memo(({
}) => {
const { currentTheme } = useTheme();
const { showSaved, showTraktSaved, showRemoved, showTraktRemoved, showSuccess, showInfo } = useToast();
-
+
// Performance optimization: Cache theme colors
const themeColors = useMemo(() => ({
white: currentTheme.colors.white,
black: '#000',
primary: currentTheme.colors.primary
}), [currentTheme.colors.white, currentTheme.colors.primary]);
-
+
// Optimized navigation handler with useCallback
const handleRatingsPress = useCallback(async () => {
// Early return if no ID
if (!id) return;
-
+
let finalTmdbId: number | null = null;
-
+
if (id.startsWith('tmdb:')) {
const numericPart = id.split(':')[1];
const parsedId = parseInt(numericPart, 10);
@@ -187,7 +187,7 @@ const ActionButtons = memo(({
finalTmdbId = parsedId;
}
}
-
+
if (finalTmdbId !== null) {
// Use requestAnimationFrame for smoother navigation
requestAnimationFrame(() => {
@@ -199,15 +199,15 @@ const ActionButtons = memo(({
// Enhanced save handler that combines local library + Trakt watchlist
const handleSaveAction = useCallback(async () => {
const wasInLibrary = inLibrary;
-
+
// Always toggle local library first
toggleLibrary();
-
+
// If authenticated, also toggle Trakt watchlist
if (isAuthenticated && onToggleWatchlist) {
await onToggleWatchlist();
}
-
+
// Show appropriate toast
if (isAuthenticated) {
if (wasInLibrary) {
@@ -227,12 +227,12 @@ const ActionButtons = memo(({
// Enhanced collection handler with toast notifications
const handleCollectionAction = useCallback(async () => {
const wasInCollection = isInCollection;
-
+
// Toggle collection
if (onToggleCollection) {
await onToggleCollection();
}
-
+
// Show appropriate toast
if (wasInCollection) {
showInfo('Removed from Collection', 'Removed from your Trakt collection');
@@ -295,10 +295,10 @@ const ActionButtons = memo(({
// For watched episodes, check if next episode exists
const nextEpisode = episodeNum + 1;
const currentSeasonEpisodes = groupedEpisodes[seasonNum] || [];
- const nextEpisodeExists = currentSeasonEpisodes.some(ep =>
+ const nextEpisodeExists = currentSeasonEpisodes.some(ep =>
ep.episode_number === nextEpisode
);
-
+
if (nextEpisodeExists) {
// Show the NEXT episode number only if it exists
const seasonStr = seasonNum.toString().padStart(2, '0');
@@ -311,10 +311,10 @@ const ActionButtons = memo(({
} else {
// For non-watched episodes, check if current episode exists
const currentSeasonEpisodes = groupedEpisodes[seasonNum] || [];
- const currentEpisodeExists = currentSeasonEpisodes.some(ep =>
+ const currentEpisodeExists = currentSeasonEpisodes.some(ep =>
ep.episode_number === episodeNum
);
-
+
if (currentEpisodeExists) {
// Current episode exists, use original button text
return playButtonText;
@@ -336,141 +336,141 @@ const ActionButtons = memo(({
// Count additional buttons (excluding Play and Save) - AI Chat no longer counted
const hasTraktCollection = isAuthenticated;
const hasRatings = type === 'series';
-
+
// Count additional buttons (AI Chat removed - now in top right corner)
const additionalButtonCount = (hasTraktCollection ? 1 : 0) + (hasRatings ? 1 : 0);
-
+
return (
{/* Single Row Layout - Play, Save, and optionally Collection/Ratings */}
-
- {
- if (isWatched) {
- return type === 'movie' ? 'replay' : 'play-arrow';
- }
- return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow';
- })()}
- size={isTablet ? 28 : 24}
- color={isWatched && type === 'movie' ? "#fff" : "#000"}
- />
- {finalPlayButtonText}
-
+
+ {
+ if (isWatched) {
+ return type === 'movie' ? 'replay' : 'play-arrow';
+ }
+ return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow';
+ })()}
+ size={isTablet ? 28 : 24}
+ color={isWatched && type === 'movie' ? "#fff" : "#000"}
+ />
+ {finalPlayButtonText}
+
+
+ {Platform.OS === 'ios' ? (
+ GlassViewComp && liquidGlassAvailable ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+ {inLibrary ? 'Saved' : 'Save'}
+
+
+
+ {/* Trakt Collection Button */}
+ {hasTraktCollection && (
{Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? (
) : (
-
+
)
) : (
-
+
)}
-
- {inLibrary ? 'Saved' : 'Save'}
-
+ )}
- {/* Trakt Collection Button */}
- {hasTraktCollection && (
-
- {Platform.OS === 'ios' ? (
- GlassViewComp && liquidGlassAvailable ? (
-
- ) : (
-
- )
+ {/* Ratings Button (for series) */}
+ {hasRatings && (
+
+ {Platform.OS === 'ios' ? (
+ GlassViewComp && liquidGlassAvailable ? (
+
) : (
-
- )}
-
-
- )}
-
- {/* Ratings Button (for series) */}
- {hasRatings && (
-
- {Platform.OS === 'ios' ? (
- GlassViewComp && liquidGlassAvailable ? (
-
- ) : (
-
- )
- ) : (
-
- )}
-
-
- )}
+
+ )
+ ) : (
+
+ )}
+
+
+ )}
);
});
// Enhanced WatchProgress Component with Trakt integration and watched status
-const WatchProgressDisplay = memo(({
- watchProgress,
- type,
- getEpisodeDetails,
+const WatchProgressDisplay = memo(({
+ watchProgress,
+ type,
+ getEpisodeDetails,
animatedStyle,
isWatched,
isTrailerPlaying,
trailerMuted,
trailerReady
}: {
- watchProgress: {
- currentTime: number;
- duration: number;
- lastUpdated: number;
+ watchProgress: {
+ currentTime: number;
+ duration: number;
+ lastUpdated: number;
episodeId?: string;
traktSynced?: boolean;
traktProgress?: number;
@@ -485,11 +485,11 @@ const WatchProgressDisplay = memo(({
}) => {
const { currentTheme } = useTheme();
const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext();
-
+
// State to trigger refresh after manual sync
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [isSyncing, setIsSyncing] = useState(false);
-
+
// Animated values for enhanced effects
const completionGlow = useSharedValue(0);
const celebrationScale = useSharedValue(1);
@@ -498,7 +498,7 @@ const WatchProgressDisplay = memo(({
const progressBoxScale = useSharedValue(0.8);
const progressBoxTranslateY = useSharedValue(20);
const syncRotation = useSharedValue(0);
-
+
// Animate the sync icon when syncing
useEffect(() => {
if (isSyncing) {
@@ -511,7 +511,7 @@ const WatchProgressDisplay = memo(({
syncRotation.value = 0;
}
}, [isSyncing, syncRotation]);
-
+
// Handle manual Trakt sync
const handleTraktSync = useMemo(() => async () => {
if (isTraktAuthenticated && forceSyncTraktProgress) {
@@ -520,7 +520,7 @@ const WatchProgressDisplay = memo(({
try {
const success = await forceSyncTraktProgress();
logger.log(`[HeroSection] Manual Trakt sync ${success ? 'successful' : 'failed'}`);
-
+
// Force component to re-render after a short delay to update sync status
if (success) {
setTimeout(() => {
@@ -541,7 +541,7 @@ const WatchProgressDisplay = memo(({
const syncIconStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${syncRotation.value}deg` }],
}));
-
+
// Memoized progress calculation with Trakt integration
const progressData = useMemo(() => {
// If content is fully watched, show watched status instead of progress
@@ -553,16 +553,16 @@ const WatchProgressDisplay = memo(({
episodeInfo = ` âĸ S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
}
}
-
- const watchedDate = watchProgress?.lastUpdated
+
+ const watchedDate = watchProgress?.lastUpdated
? new Date(watchProgress.lastUpdated).toLocaleDateString('en-US')
: new Date().toLocaleDateString('en-US');
-
+
// Determine if watched via Trakt or local
- const watchedViaTrakt = isTraktAuthenticated &&
- watchProgress?.traktProgress !== undefined &&
+ const watchedViaTrakt = isTraktAuthenticated &&
+ watchProgress?.traktProgress !== undefined &&
watchProgress.traktProgress >= 95;
-
+
return {
progressPercent: 100,
formattedTime: watchedDate,
@@ -579,7 +579,7 @@ const WatchProgressDisplay = memo(({
// Determine which progress to show - prioritize Trakt if available and authenticated
let progressPercent;
let isUsingTraktProgress = false;
-
+
if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
progressPercent = watchProgress.traktProgress;
isUsingTraktProgress = true;
@@ -599,7 +599,7 @@ const WatchProgressDisplay = memo(({
// Enhanced display text with Trakt integration
let displayText = progressPercent >= 85 ? 'Watched' : `${Math.round(progressPercent)}% watched`;
let syncStatus = '';
-
+
// Show Trakt sync status if user is authenticated
if (isTraktAuthenticated) {
if (isUsingTraktProgress) {
@@ -610,8 +610,8 @@ const WatchProgressDisplay = memo(({
} else if (watchProgress.traktSynced) {
syncStatus = ' âĸ Synced with Trakt';
// If we have specific Trakt progress that differs from local, mention it
- if (watchProgress.traktProgress !== undefined &&
- Math.abs(progressPercent - watchProgress.traktProgress) > 5) {
+ if (watchProgress.traktProgress !== undefined &&
+ Math.abs(progressPercent - watchProgress.traktProgress) > 5) {
displayText = `${Math.round(progressPercent)}% watched (${Math.round(watchProgress.traktProgress)}% on Trakt)`;
}
} else {
@@ -638,7 +638,7 @@ const WatchProgressDisplay = memo(({
progressBoxOpacity.value = withTiming(1, { duration: 400 });
progressBoxScale.value = withTiming(1, { duration: 400 });
progressBoxTranslateY.value = withTiming(0, { duration: 400 });
-
+
if (progressData.isWatched || (progressData.progressPercent && progressData.progressPercent >= 85)) {
// Celebration animation sequence
celebrationScale.value = withRepeat(
@@ -646,7 +646,7 @@ const WatchProgressDisplay = memo(({
2,
true
);
-
+
// Glow effect
completionGlow.value = withRepeat(
withTiming(1, { duration: 1500 }),
@@ -712,34 +712,34 @@ const WatchProgressDisplay = memo(({
) : (
)}
-
+
{/* Enhanced progress bar with glow effects */}
-
+
{/* Background glow for completed content */}
{isCompleted && (
)}
-
-
-
+ }
+ ]}
+ />
+
{/* Shimmer effect for active progress */}
{!isCompleted && progressData.progressPercent > 0 && (
@@ -768,46 +768,46 @@ const WatchProgressDisplay = memo(({
{progressData.episodeInfo}
)}
-
+
{/* Trakt sync status with enhanced styling */}
{progressData.syncStatus && (
-
- {progressData.syncStatus}
-
-
+ {progressData.syncStatus}
+
+
{/* Enhanced manual Trakt sync button - moved inline */}
- {isTraktAuthenticated && forceSyncTraktProgress && (
-
-
-
-
+ >
+
+
+
-
+
)}
- )}
-
+ )}
+
);
@@ -896,12 +896,12 @@ const HeroSection: React.FC = memo(({
// Guards to avoid repeated auto-starts
const startedOnFocusRef = useRef(false);
const startedOnReadyRef = useRef(false);
-
+
// Animation values for trailer unmute effects
const actionButtonsOpacity = useSharedValue(1);
const titleCardTranslateY = useSharedValue(0);
const genreOpacity = useSharedValue(1);
-
+
// Ultra-optimized theme colors with stable references
const themeColors = useMemo(() => ({
black: currentTheme.colors.black,
@@ -932,7 +932,7 @@ const HeroSection: React.FC = memo(({
setTrailerPreloaded(true);
}
setTrailerReady(true);
-
+
// Smooth transition: fade out thumbnail, fade in trailer
thumbnailOpacity.value = withTiming(0, { duration: 500 });
trailerOpacity.value = withTiming(1, { duration: 500 });
@@ -948,7 +948,7 @@ const HeroSection: React.FC = memo(({
try {
const y = (scrollY as any).value || 0;
const pauseThreshold = heroHeight.value * 0.7;
-
+
if (y < pauseThreshold) {
startedOnReadyRef.current = true;
logger.info('HeroSection', 'Trailer ready - auto-starting playback');
@@ -987,7 +987,7 @@ const HeroSection: React.FC = memo(({
setTrailerError(true);
setTrailerReady(false);
setTrailerPlaying(false);
-
+
// Fade back to thumbnail
trailerOpacity.value = withTiming(0, { duration: 300 });
thumbnailOpacity.value = withTiming(1, { duration: 300 });
@@ -997,11 +997,11 @@ const HeroSection: React.FC = memo(({
const handleTrailerEnd = useCallback(async () => {
logger.info('HeroSection', 'Trailer ended - transitioning back to thumbnail');
setTrailerPlaying(false);
-
+
// Reset trailer state to prevent auto-restart
setTrailerReady(false);
setTrailerPreloaded(false);
-
+
// If trailer is in fullscreen, dismiss it first
try {
if (trailerVideoRef.current) {
@@ -1011,22 +1011,22 @@ const HeroSection: React.FC = memo(({
} catch (error) {
logger.warn('HeroSection', 'Error dismissing fullscreen player:', error);
}
-
+
// Smooth fade transition: trailer out, thumbnail in
trailerOpacity.value = withTiming(0, { duration: 500 });
thumbnailOpacity.value = withTiming(1, { duration: 500 });
-
+
// Show UI elements again
actionButtonsOpacity.value = withTiming(1, { duration: 500 });
genreOpacity.value = withTiming(1, { duration: 500 });
titleCardTranslateY.value = withTiming(0, { duration: 500 });
watchProgressOpacity.value = withTiming(1, { duration: 500 });
}, [trailerOpacity, thumbnailOpacity, actionButtonsOpacity, genreOpacity, titleCardTranslateY, watchProgressOpacity, setTrailerPlaying]);
-
+
// Memoized image source
- const imageSource = useMemo(() =>
+ const imageSource = useMemo(() =>
bannerImage || metadata.banner || metadata.poster
- , [bannerImage, metadata.banner, metadata.poster]);
+ , [bannerImage, metadata.banner, metadata.poster]);
// Use the logo provided by metadata (already enriched by useMetadataAssets based on settings)
const logoUri = useMemo(() => {
@@ -1048,13 +1048,13 @@ const HeroSection: React.FC = memo(({
useEffect(() => {
// Check if metadata logo has actually changed from what we last processed
const currentMetadataLogo = metadata?.logo;
-
+
if (currentMetadataLogo !== lastSyncedLogoRef.current) {
lastSyncedLogoRef.current = currentMetadataLogo;
// Reset text fallback and timers on logo updates
if (logoWaitTimerRef.current) {
- try { clearTimeout(logoWaitTimerRef.current); } catch (_e) {}
+ try { clearTimeout(logoWaitTimerRef.current); } catch (_e) { }
logoWaitTimerRef.current = null;
}
@@ -1076,10 +1076,10 @@ const HeroSection: React.FC = memo(({
}, 600);
}
}
-
+
return () => {
if (logoWaitTimerRef.current) {
- try { clearTimeout(logoWaitTimerRef.current); } catch (_e) {}
+ try { clearTimeout(logoWaitTimerRef.current); } catch (_e) { }
logoWaitTimerRef.current = null;
}
};
@@ -1109,7 +1109,7 @@ const HeroSection: React.FC = memo(({
}
// If logo loaded successfully before, keep showing it even if it fails later
}, [logoHasLoadedSuccessfully, stableLogoUri, metadata, logoLoadOpacity]);
-
+
// Performance optimization: Lazy loading setup
useEffect(() => {
const timer = InteractionManager.runAfterInteractions(() => {
@@ -1118,7 +1118,7 @@ const HeroSection: React.FC = memo(({
setShouldLoadSecondaryData(true);
}
});
-
+
return () => timer.cancel();
}, []);
@@ -1128,25 +1128,25 @@ const HeroSection: React.FC = memo(({
let timerId: any = null;
const fetchTrailer = async () => {
if (!metadata?.name || !metadata?.year || !settings?.showTrailers || !isFocused) return;
-
+
// If we expect TMDB ID but don't have it yet, wait a bit more
if (!metadata?.tmdbId && metadata?.id?.startsWith('tmdb:')) {
logger.info('HeroSection', `Waiting for TMDB ID for ${metadata.name}`);
return;
}
-
+
setTrailerLoading(true);
setTrailerError(false);
setTrailerReady(false);
setTrailerPreloaded(false);
-
+
try {
// Use requestIdleCallback or setTimeout to prevent blocking main thread
const fetchWithDelay = () => {
// Extract TMDB ID if available
const tmdbIdString = tmdbId ? String(tmdbId) : undefined;
const contentType = type === 'series' ? 'tv' : 'movie';
-
+
// Debug logging to see what we have
logger.info('HeroSection', `Trailer request for ${metadata.name}:`, {
hasTmdbId: !!tmdbId,
@@ -1155,7 +1155,7 @@ const HeroSection: React.FC = memo(({
metadataKeys: Object.keys(metadata || {}),
metadataId: metadata?.id
});
-
+
TrailerService.getTrailerUrl(metadata.name, metadata.year, tmdbIdString, contentType)
.then(url => {
if (url) {
@@ -1190,12 +1190,12 @@ const HeroSection: React.FC = memo(({
fetchTrailer();
return () => {
alive = false;
- try { if (timerId) clearTimeout(timerId); } catch (_e) {}
+ try { if (timerId) clearTimeout(timerId); } catch (_e) { }
};
}, [metadata?.name, metadata?.year, tmdbId, settings?.showTrailers, isFocused]);
// Shimmer animation removed
-
+
// Optimized loading state reset when image source changes
useEffect(() => {
if (imageSource) {
@@ -1203,19 +1203,19 @@ const HeroSection: React.FC = memo(({
imageLoadOpacity.value = 0;
}
}, [imageSource]);
-
+
// Optimized image handlers with useCallback
const handleImageError = useCallback(() => {
if (!shouldLoadSecondaryData) return;
-
+
runOnUI(() => {
imageOpacity.value = withTiming(0.6, { duration: 150 });
imageLoadOpacity.value = withTiming(0, { duration: 150 });
})();
-
+
setImageError(true);
setImageLoaded(false);
-
+
// Three-level fallback: TMDB â addon banner â poster
if (bannerImage !== metadata.banner && metadata.banner) {
// Try addon banner if not already on it and it exists
@@ -1231,7 +1231,7 @@ const HeroSection: React.FC = memo(({
imageOpacity.value = withTiming(1, { duration: 150 });
imageLoadOpacity.value = withTiming(1, { duration: 400 });
})();
-
+
setImageError(false);
setImageLoaded(true);
}, []);
@@ -1245,10 +1245,10 @@ const HeroSection: React.FC = memo(({
const logoAnimatedStyle = useAnimatedStyle(() => {
// Determine if progress bar should be shown
const hasProgress = watchProgress && watchProgress.duration > 0;
-
+
// Scale down logo when progress bar is present
const logoScale = hasProgress ? 0.85 : 1;
-
+
return {
opacity: logoOpacity.value,
transform: [
@@ -1271,22 +1271,22 @@ const HeroSection: React.FC = memo(({
const backdropImageStyle = useAnimatedStyle(() => {
'worklet';
const scrollYValue = scrollY.value;
-
+
// Pre-calculated constants for better performance
const DEFAULT_ZOOM = 1.1;
const SCROLL_UP_MULTIPLIER = 0.002;
const SCROLL_DOWN_MULTIPLIER = 0.0001;
const MAX_SCALE = 1.4;
const PARALLAX_FACTOR = 0.3;
-
+
// Optimized scale calculation with minimal branching
const scrollUpScale = DEFAULT_ZOOM + Math.abs(scrollYValue) * SCROLL_UP_MULTIPLIER;
const scrollDownScale = DEFAULT_ZOOM + scrollYValue * SCROLL_DOWN_MULTIPLIER;
const scale = Math.min(scrollYValue < 0 ? scrollUpScale : scrollDownScale, MAX_SCALE);
-
+
// Single parallax calculation
const parallaxOffset = scrollYValue * PARALLAX_FACTOR;
-
+
return {
opacity: imageOpacity.value * imageLoadOpacity.value,
transform: [
@@ -1299,7 +1299,7 @@ const HeroSection: React.FC = memo(({
// Simplified buttons animation
const buttonsAnimatedStyle = useAnimatedStyle(() => ({
opacity: buttonsOpacity.value * actionButtonsOpacity.value,
- transform: [{
+ transform: [{
translateY: interpolate(
buttonsTranslateY.value,
[0, 20],
@@ -1323,22 +1323,22 @@ const HeroSection: React.FC = memo(({
const trailerParallaxStyle = useAnimatedStyle(() => {
'worklet';
const scrollYValue = scrollY.value;
-
+
// Pre-calculated constants for better performance
const DEFAULT_ZOOM = 1.0;
const SCROLL_UP_MULTIPLIER = 0.0015;
const SCROLL_DOWN_MULTIPLIER = 0.0001;
const MAX_SCALE = 1.25;
const PARALLAX_FACTOR = 0.2;
-
+
// Optimized scale calculation with minimal branching
const scrollUpScale = DEFAULT_ZOOM + Math.abs(scrollYValue) * SCROLL_UP_MULTIPLIER;
const scrollDownScale = DEFAULT_ZOOM + scrollYValue * SCROLL_DOWN_MULTIPLIER;
const scale = Math.min(scrollYValue < 0 ? scrollUpScale : scrollDownScale, MAX_SCALE);
-
+
// Single parallax calculation
const parallaxOffset = scrollYValue * PARALLAX_FACTOR;
-
+
return {
transform: [
{ scale },
@@ -1394,14 +1394,14 @@ const HeroSection: React.FC = memo(({
// Calculate if content is watched (>=85% progress) - check both local and Trakt progress
const isWatched = useMemo(() => {
if (!watchProgress) return false;
-
+
// Check Trakt progress first if available and user is authenticated
if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
const traktWatched = watchProgress.traktProgress >= 95;
// Removed excessive logging for Trakt progress
return traktWatched;
}
-
+
// Fall back to local progress
if (watchProgress.duration === 0) return false;
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
@@ -1457,7 +1457,7 @@ const HeroSection: React.FC = memo(({
}
}, 50);
}
-
+
return () => {
// Stop trailer when leaving this screen to prevent background playback/heat
logger.info('HeroSection', 'Screen unfocused - stopping trailer playback');
@@ -1490,7 +1490,7 @@ const HeroSection: React.FC = memo(({
setTrailerUrl(null);
trailerOpacity.value = 0;
thumbnailOpacity.value = 1;
- } catch (_e) {}
+ } catch (_e) { }
}
}, [isFocused, setTrailerPlaying]);
@@ -1499,7 +1499,7 @@ const HeroSection: React.FC = memo(({
'worklet';
try {
if (!scrollGuardEnabledSV.value || isFocusedSV.value === 0) return;
-
+
// Pre-calculate thresholds for better performance
const pauseThreshold = heroHeight.value * 0.7;
const resumeThreshold = heroHeight.value * 0.4;
@@ -1528,7 +1528,7 @@ const HeroSection: React.FC = memo(({
// Don't stop trailer playback when component unmounts
// Let the new hero section (if any) take control of trailer state
// This prevents the trailer from stopping when navigating between screens
-
+
// Reset animation values on unmount to prevent memory leaks
try {
imageOpacity.value = 1;
@@ -1548,7 +1548,7 @@ const HeroSection: React.FC = memo(({
} catch (error) {
logger.error('HeroSection', 'Error cleaning up animation values:', error);
}
-
+
interactionComplete.current = false;
};
}, [imageOpacity, imageLoadOpacity, trailerOpacity, thumbnailOpacity, actionButtonsOpacity, titleCardTranslateY, genreOpacity, watchProgressOpacity, buttonsOpacity, buttonsTranslateY, logoOpacity, heroOpacity, heroHeight]);
@@ -1574,46 +1574,46 @@ const HeroSection: React.FC = memo(({
{/* Optimized Background */}
-
- {/* Shimmer loading effect removed */}
-
- {/* Background thumbnail image - always rendered when available with parallax */}
- {shouldLoadSecondaryData && imageSource && !loadingBanner && (
-
-
-
- )}
-
- {/* Hidden preload trailer player - loads in background */}
- {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && !trailerPreloaded && (
-
-
-
- )}
-
- {/* Visible trailer player - rendered on top with fade transition and parallax */}
- {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && (
-
-
+
+
+ )}
+
+ {/* Hidden preload trailer player - loads in background */}
+ {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && !trailerPreloaded && (
+
+
+
+ )}
+
+ {/* Visible trailer player - rendered on top with fade transition and parallax */}
+ {shouldLoadSecondaryData && settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && (
+
+ = memo(({
}
}}
/>
-
- )}
+
+ )}
- {/* Trailer control buttons (unmute and fullscreen) */}
- {settings?.showTrailers && trailerReady && trailerUrl && (
- = 768 ? 32 : 16,
- zIndex: 1000,
- opacity: trailerOpacity,
- flexDirection: 'row',
- gap: 8,
- }}>
- {/* Fullscreen button */}
- e.stopPropagation()}
- onPressOut={(e) => e.stopPropagation()}
- style={{
- padding: 8,
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- borderRadius: 20,
- }}
- >
-
-
+ {/* Trailer control buttons (unmute and fullscreen) */}
+ {settings?.showTrailers && trailerReady && trailerUrl && (
+ = 768 ? 32 : 16,
+ zIndex: 1000,
+ opacity: trailerOpacity,
+ flexDirection: 'row',
+ gap: 8,
+ }}>
+ {/* Fullscreen button */}
+ e.stopPropagation()}
+ onPressOut={(e) => e.stopPropagation()}
+ style={{
+ padding: 8,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ borderRadius: 20,
+ }}
+ >
+
+
- {/* Unmute button */}
- {
- logger.info('HeroSection', 'Mute toggle button pressed, current muted state:', trailerMuted);
- updateSetting('trailerMuted', !trailerMuted);
- if (trailerMuted) {
- // When unmuting, hide action buttons, genre, title card, and watch progress
- actionButtonsOpacity.value = withTiming(0, { duration: 300 });
- genreOpacity.value = withTiming(0, { duration: 300 });
- titleCardTranslateY.value = withTiming(100, { duration: 300 }); // Increased from 60 to 120 for further down movement
- watchProgressOpacity.value = withTiming(0, { duration: 300 });
- } else {
- // When muting, show action buttons, genre, title card, and watch progress
- actionButtonsOpacity.value = withTiming(1, { duration: 300 });
- genreOpacity.value = withTiming(1, { duration: 300 });
- titleCardTranslateY.value = withTiming(0, { duration: 300 });
- watchProgressOpacity.value = withTiming(1, { duration: 300 });
- }
- }}
- activeOpacity={0.7}
- onPressIn={(e) => e.stopPropagation()}
- onPressOut={(e) => e.stopPropagation()}
- style={{
- padding: 8,
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- borderRadius: 20,
- }}
- >
-
-
+ {/* Unmute button */}
+ {
+ logger.info('HeroSection', 'Mute toggle button pressed, current muted state:', trailerMuted);
+ updateSetting('trailerMuted', !trailerMuted);
+ if (trailerMuted) {
+ // When unmuting, hide action buttons, genre, title card, and watch progress
+ actionButtonsOpacity.value = withTiming(0, { duration: 300 });
+ genreOpacity.value = withTiming(0, { duration: 300 });
+ titleCardTranslateY.value = withTiming(100, { duration: 300 }); // Increased from 60 to 120 for further down movement
+ watchProgressOpacity.value = withTiming(0, { duration: 300 });
+ } else {
+ // When muting, show action buttons, genre, title card, and watch progress
+ actionButtonsOpacity.value = withTiming(1, { duration: 300 });
+ genreOpacity.value = withTiming(1, { duration: 300 });
+ titleCardTranslateY.value = withTiming(0, { duration: 300 });
+ watchProgressOpacity.value = withTiming(1, { duration: 300 });
+ }
+ }}
+ activeOpacity={0.7}
+ onPressIn={(e) => e.stopPropagation()}
+ onPressOut={(e) => e.stopPropagation()}
+ style={{
+ padding: 8,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ borderRadius: 20,
+ }}
+ >
+
+
- {/* AI Chat button */}
- {settings?.aiChatEnabled && (
+ {/* AI Chat button */}
+ {settings?.aiChatEnabled && (
+ {
+ // Extract episode info if it's a series
+ let episodeData = null;
+ if (type === 'series' && watchProgress && watchProgress.episodeId) {
+ const parts = watchProgress.episodeId.split(':');
+ if (parts.length >= 3) {
+ episodeData = {
+ seasonNumber: parseInt(parts[1], 10),
+ episodeNumber: parseInt(parts[2], 10)
+ };
+ }
+ }
+
+ navigation.navigate('AIChat', {
+ contentId: id,
+ contentType: type,
+ episodeId: episodeData && watchProgress ? watchProgress.episodeId : undefined,
+ seasonNumber: episodeData?.seasonNumber,
+ episodeNumber: episodeData?.episodeNumber,
+ title: metadata?.name || metadata?.title || 'Unknown'
+ });
+ }}
+ activeOpacity={0.7}
+ onPressIn={(e) => e.stopPropagation()}
+ onPressOut={(e) => e.stopPropagation()}
+ style={{
+ padding: 8,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ borderRadius: 20,
+ }}
+ >
+
+
+ )}
+
+ )}
+
+ {/* AI Chat button (when trailers are disabled) */}
+ {settings?.aiChatEnabled && !(settings?.showTrailers && trailerReady && trailerUrl) && (
+ = 768 ? 32 : 16,
+ zIndex: 1000,
+ }}>
{
// Extract episode info if it's a series
@@ -1726,8 +1776,6 @@ const HeroSection: React.FC = memo(({
});
}}
activeOpacity={0.7}
- onPressIn={(e) => e.stopPropagation()}
- onPressOut={(e) => e.stopPropagation()}
style={{
padding: 8,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
@@ -1740,164 +1788,116 @@ const HeroSection: React.FC = memo(({
color="white"
/>
- )}
-
- )}
+
+ )}
- {/* AI Chat button (when trailers are disabled) */}
- {settings?.aiChatEnabled && !(settings?.showTrailers && trailerReady && trailerUrl) && (
- = 768 ? 32 : 16,
- zIndex: 1000,
- }}>
- {
- // Extract episode info if it's a series
- let episodeData = null;
- if (type === 'series' && watchProgress && watchProgress.episodeId) {
- const parts = watchProgress.episodeId.split(':');
- if (parts.length >= 3) {
- episodeData = {
- seasonNumber: parseInt(parts[1], 10),
- episodeNumber: parseInt(parts[2], 10)
- };
- }
- }
-
- navigation.navigate('AIChat', {
- contentId: id,
- contentType: type,
- episodeId: episodeData && watchProgress ? watchProgress.episodeId : undefined,
- seasonNumber: episodeData?.seasonNumber,
- episodeNumber: episodeData?.episodeNumber,
- title: metadata?.name || metadata?.title || 'Unknown'
- });
- }}
- activeOpacity={0.7}
- style={{
- padding: 8,
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- borderRadius: 20,
- }}
- >
+
+
- )}
-
-
-
-
-
-
- {/* Ultra-light Gradient with subtle dynamic background blend */}
-
- {/* Enhanced bottom fade with stronger gradient */}
+ {/* Ultra-light Gradient with subtle dynamic background blend */}
-
- {/* Optimized Title/Logo - Show logo immediately when available */}
-
-
- {metadata?.logo ? (
-
- ) : shouldShowTextFallback ? (
-
- {metadata.name}
-
- ) : (
- // Reserve space to prevent layout jump while waiting briefly for logo
-
- )}
-
-
-
- {/* Enhanced Watch Progress with Trakt integration */}
-
+ {/* Enhanced bottom fade with stronger gradient */}
+
-
- {/* Optimized genre display with lazy loading; no fixed blank space */}
- {shouldLoadSecondaryData && genreElements && (
-
- {genreElements}
+
+ {/* Optimized Title/Logo - Show logo immediately when available */}
+
+
+ {metadata?.logo ? (
+
+ ) : shouldShowTextFallback ? (
+
+ {metadata.name}
+
+ ) : (
+ // Reserve space to prevent layout jump while waiting briefly for logo
+
+ )}
+
- )}
+
+ {/* Enhanced Watch Progress with Trakt integration */}
+
+
+ {/* Optimized genre display with lazy loading; no fixed blank space */}
+ {shouldLoadSecondaryData && genreElements && (
+
+ {genreElements}
+
+ )}
- {/* Optimized Action Buttons */}
-
-
-
+ {/* Optimized Action Buttons */}
+
+
+
);
@@ -2429,7 +2429,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
-
+
// Tablet-specific styles
tabletActionButtons: {
flexDirection: 'column',
@@ -2531,27 +2531,27 @@ const styles = StyleSheet.create({
alignSelf: 'center',
},
tabletProgressGlassBackground: {
- width: width * 0.7,
- maxWidth: 700,
- backgroundColor: 'rgba(255,255,255,0.08)',
- borderRadius: 16,
- padding: 12,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.1)',
- overflow: 'hidden',
- alignSelf: 'center',
- },
- tabletWatchProgressMainText: {
- fontSize: 14,
- fontWeight: '600',
- textAlign: 'center',
- },
- tabletWatchProgressSubText: {
- fontSize: 12,
- textAlign: 'center',
- opacity: 0.8,
- marginBottom: 1,
- },
+ width: width * 0.7,
+ maxWidth: 700,
+ backgroundColor: 'rgba(255,255,255,0.08)',
+ borderRadius: 16,
+ padding: 12,
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.1)',
+ overflow: 'hidden',
+ alignSelf: 'center',
+ },
+ tabletWatchProgressMainText: {
+ fontSize: 14,
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ tabletWatchProgressSubText: {
+ fontSize: 12,
+ textAlign: 'center',
+ opacity: 0.8,
+ marginBottom: 1,
+ },
});
export default HeroSection;
diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts
index ac7192ee..b40428df 100644
--- a/src/hooks/useMetadata.ts
+++ b/src/hooks/useMetadata.ts
@@ -1569,6 +1569,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setScraperStatuses(initialStatuses);
setActiveFetchingScrapers(initialActiveFetching);
console.log('đ [loadStreams] Initialized activeFetchingScrapers:', initialActiveFetching);
+
+ // If no scrapers are available, stop loading immediately
+ if (initialStatuses.length === 0) {
+ setLoadingStreams(false);
+ }
} catch (error) {
if (__DEV__) console.error('Failed to initialize scraper tracking:', error);
}
@@ -1701,6 +1706,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setScraperStatuses(initialStatuses);
setActiveFetchingScrapers(initialActiveFetching);
console.log('đ [loadEpisodeStreams] Initialized activeFetchingScrapers:', initialActiveFetching);
+
+ // If no scrapers are available, stop loading immediately
+ if (initialStatuses.length === 0) {
+ setLoadingEpisodeStreams(false);
+ }
} catch (error) {
if (__DEV__) console.error('Failed to initialize episode scraper tracking:', error);
}
@@ -1715,6 +1725,37 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
const { isCollection: detectedCollection, addon: collectionAddon } = stremioService.isCollectionContent(id);
isCollection = detectedCollection;
+
+ // Parse season and episode numbers robustly
+ let showIdStr = id;
+ let seasonNum = '';
+ let episodeNum = '';
+
+ try {
+ // Handle various episode ID formats
+ // 1. Internal format: "series:showId:season:episode"
+ // 2. Stremio/IMDb format: "tt12345:1:1"
+ // 3. TMDB format: "tmdb:123:1:1"
+
+ const cleanEpisodeId = episodeId.replace(/^series:/, '');
+ const parts = cleanEpisodeId.split(':');
+
+ if (parts.length >= 3) {
+ episodeNum = parts.pop() || '';
+ seasonNum = parts.pop() || '';
+ showIdStr = parts.join(':');
+ } else if (parts.length === 2) {
+ // Edge case: maybe just id:episode? unlikely but safe fallback
+ episodeNum = parts[1];
+ seasonNum = '1'; // Default
+ showIdStr = parts[0];
+ }
+
+ if (__DEV__) console.log(`đ [loadEpisodeStreams] Parsed ID: show=${showIdStr}, s=${seasonNum}, e=${episodeNum}`);
+ } catch (e) {
+ if (__DEV__) console.warn('â ī¸ [loadEpisodeStreams] Failed to parse episode ID:', episodeId);
+ }
+
if (isCollection && collectionAddon) {
if (__DEV__) console.log(`đŦ [loadEpisodeStreams] Detected collection from addon: ${collectionAddon.name}, treating episodes as individual movies`);
@@ -1728,9 +1769,15 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
stremioEpisodeId = episodeId; // Use the IMDb ID directly for Stremio addons
if (__DEV__) console.log('â
[loadEpisodeStreams] Collection movie - using IMDb ID:', episodeId, 'TMDB ID:', tmdbId);
} else {
- // Fallback: try to parse as TMDB ID
- tmdbId = episodeId;
- stremioEpisodeId = episodeId;
+ // Fallback: try to verify if it's a tmdb id
+ const isTmdb = episodeId.startsWith('tmdb:') || !isNaN(Number(episodeId));
+ if (isTmdb) {
+ const cleanId = episodeId.replace('tmdb:', '');
+ tmdbId = cleanId;
+ stremioEpisodeId = episodeId;
+ } else {
+ stremioEpisodeId = episodeId;
+ }
if (__DEV__) console.log('â ī¸ [loadEpisodeStreams] Collection movie - using episodeId as-is:', episodeId);
}
} else if (id.startsWith('tmdb:')) {
@@ -1739,13 +1786,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// Try to get IMDb ID from metadata first, then convert if needed
if (metadata?.imdb_id) {
- // Replace the series ID in episodeId with the IMDb ID
- const [, season, episode] = episodeId.split(':');
- stremioEpisodeId = `${metadata.imdb_id}:${season}:${episode}`;
+ // Use format: imdb_id:season:episode
+ stremioEpisodeId = `${metadata.imdb_id}:${seasonNum}:${episodeNum}`;
if (__DEV__) console.log('â
[loadEpisodeStreams] Using IMDb ID from metadata for Stremio episode:', stremioEpisodeId);
} else if (imdbId) {
- const [, season, episode] = episodeId.split(':');
- stremioEpisodeId = `${imdbId}:${season}:${episode}`;
+ stremioEpisodeId = `${imdbId}:${seasonNum}:${episodeNum}`;
if (__DEV__) console.log('â
[loadEpisodeStreams] Using stored IMDb ID for Stremio episode:', stremioEpisodeId);
} else {
// Convert TMDB ID to IMDb ID for Stremio addons
@@ -1753,14 +1798,17 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
const externalIds = await withTimeout(tmdbService.getShowExternalIds(parseInt(tmdbId)), API_TIMEOUT);
if (externalIds?.imdb_id) {
- const [, season, episode] = episodeId.split(':');
- stremioEpisodeId = `${externalIds.imdb_id}:${season}:${episode}`;
+ stremioEpisodeId = `${externalIds.imdb_id}:${seasonNum}:${episodeNum}`;
if (__DEV__) console.log('â
[loadEpisodeStreams] Converted TMDB to IMDb ID for Stremio episode:', stremioEpisodeId);
} else {
- if (__DEV__) console.log('â ī¸ [loadEpisodeStreams] No IMDb ID found for TMDB ID, using original episode ID:', stremioEpisodeId);
+ // Fallback to TMDB format if conversions fail
+ // e.g. tmdb:123:1:1
+ stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
+ if (__DEV__) console.log('â ī¸ [loadEpisodeStreams] No IMDb ID found for TMDB ID, using TMDB episode ID:', stremioEpisodeId);
}
} catch (error) {
- if (__DEV__) console.log('â ī¸ [loadEpisodeStreams] Failed to convert TMDB to IMDb, using original episode ID:', error);
+ stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
+ if (__DEV__) console.log('â ī¸ [loadEpisodeStreams] Failed to convert TMDB to IMDb, using TMDB episode ID:', error);
}
}
} else if (id.startsWith('tt')) {
@@ -1772,20 +1820,18 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
if (__DEV__) console.log('đ [loadEpisodeStreams] TMDB enrichment disabled, skipping IMDB to TMDB conversion');
}
if (__DEV__) console.log('â
[loadEpisodeStreams] Converted to TMDB ID:', tmdbId);
- // Normalize episode id to 'tt:season:episode' format for addons that expect tt prefix
- const parts = episodeId.split(':');
- if (parts.length === 3 && parts[0] === 'series') {
- stremioEpisodeId = `${id}:${parts[1]}:${parts[2]}`;
- if (__DEV__) console.log('đ§ [loadEpisodeStreams] Normalized episode ID for addons:', stremioEpisodeId);
- }
+
+ // Ensure consistent format
+ stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
+ if (__DEV__) console.log('đ§ [loadEpisodeStreams] Normalized episode ID for addons:', stremioEpisodeId);
} else {
tmdbId = id;
+ stremioEpisodeId = `${id}:${seasonNum}:${episodeNum}`;
if (__DEV__) console.log('âšī¸ [loadEpisodeStreams] Using ID as both TMDB and Stremio ID:', tmdbId);
}
// Extract episode info from the episodeId for logging
- const [, season, episode] = episodeId.split(':');
- const episodeQuery = `?s=${season}&e=${episode}`;
+ const episodeQuery = `?s=${seasonNum}&e=${episodeNum}`;
if (__DEV__) console.log(`âšī¸ [loadEpisodeStreams] Episode query: ${episodeQuery}`);
if (__DEV__) console.log('đ [loadEpisodeStreams] Starting stream requests');
diff --git a/src/screens/streams/useStreamsScreen.ts b/src/screens/streams/useStreamsScreen.ts
index 7ab79bd0..c1b05897 100644
--- a/src/screens/streams/useStreamsScreen.ts
+++ b/src/screens/streams/useStreamsScreen.ts
@@ -57,7 +57,7 @@ export const useStreamsScreen = () => {
// Dimension tracking
const [dimensions, setDimensions] = useState(Dimensions.get('window'));
const prevDimensionsRef = useRef({ width: dimensions.width, height: dimensions.height });
-
+
const deviceWidth = dimensions.width;
const isTablet = useMemo(() => deviceWidth >= TABLET_BREAKPOINT, [deviceWidth]);
@@ -119,7 +119,7 @@ export const useStreamsScreen = () => {
} = useMetadata({ id, type });
// Get banner image
- const setMetadataStub = useCallback(() => {}, []);
+ const setMetadataStub = useCallback(() => { }, []);
const memoizedSettings = useMemo(
() => settings,
[settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]
@@ -183,7 +183,7 @@ export const useStreamsScreen = () => {
try {
setAlertTitle(title);
setAlertMessage(message);
- setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
+ setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
setAlertVisible(true);
} catch (error) {
console.warn('[StreamsScreen] Error showing alert:', error);
@@ -390,7 +390,7 @@ export const useStreamsScreen = () => {
if (!videoType && /xprime/i.test(providerId)) {
videoType = 'm3u8';
}
- } catch {}
+ } catch { }
const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid';
@@ -438,7 +438,7 @@ export const useStreamsScreen = () => {
/format=mkv\b/i.test(lowerUrl) ||
/container=mkv\b/i.test(lowerUrl);
const isHttp = lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
-
+
if (!isMkvByPath && isHttp) {
try {
const mkvDetected = await Promise.race([
@@ -609,14 +609,14 @@ export const useStreamsScreen = () => {
useEffect(() => {
// Build a unique key for the current content
const currentKey = `${id}:${type}:${episodeId || ''}`;
-
+
// Reset refs if content changed
if (lastLoadedIdRef.current !== currentKey) {
hasDoneInitialLoadRef.current = false;
isLoadingStreamsRef.current = false;
lastLoadedIdRef.current = currentKey;
}
-
+
// Only proceed if we haven't done the initial load for this content
if (hasDoneInitialLoadRef.current) return;
@@ -803,18 +803,18 @@ export const useStreamsScreen = () => {
const sortedEntries = filteredEntries.sort(([addonIdA], [addonIdB]) => {
const isAddonA = installedAddons.some(addon => addon.id === addonIdA);
const isAddonB = installedAddons.some(addon => addon.id === addonIdB);
-
+
// Addons always come before plugins
if (isAddonA && !isAddonB) return -1;
if (!isAddonA && isAddonB) return 1;
-
+
// Both are addons - sort by installation order
if (isAddonA && isAddonB) {
const indexA = installedAddons.findIndex(addon => addon.id === addonIdA);
const indexB = installedAddons.findIndex(addon => addon.id === addonIdB);
return indexA - indexB;
}
-
+
// Both are plugins - sort by response order
const responseIndexA = addonResponseOrder.indexOf(addonIdA);
const responseIndexB = addonResponseOrder.indexOf(addonIdB);
@@ -1021,8 +1021,9 @@ export const useStreamsScreen = () => {
Object.keys(streams).length === 0 ||
Object.values(streams).every(provider => !provider.streams || provider.streams.length === 0);
const loadElapsed = streamsLoadStart ? Date.now() - streamsLoadStart : 0;
- const showInitialLoading = streamsEmpty && (streamsLoadStart === null || loadElapsed < 10000);
- const showStillFetching = streamsEmpty && loadElapsed >= 10000;
+ const isActuallyLoading = isLoading || activeFetchingScrapers.length > 0;
+ const showInitialLoading = streamsEmpty && isActuallyLoading && (streamsLoadStart === null || loadElapsed < 10000);
+ const showStillFetching = streamsEmpty && isActuallyLoading && loadElapsed >= 10000;
return {
// Route params
@@ -1031,19 +1032,19 @@ export const useStreamsScreen = () => {
episodeId,
episodeThumbnail,
fromPlayer,
-
+
// Theme
currentTheme,
colors,
settings,
-
+
// Navigation
navigation,
handleBack,
-
+
// Tablet
isTablet,
-
+
// Alert
alertVisible,
alertTitle,
@@ -1051,14 +1052,14 @@ export const useStreamsScreen = () => {
alertActions,
openAlert,
closeAlert,
-
+
// Metadata
metadata,
imdbId,
bannerImage,
currentEpisode,
groupedEpisodes,
-
+
// Streams
streams,
groupedStreams,
@@ -1068,7 +1069,7 @@ export const useStreamsScreen = () => {
selectedProvider,
handleProviderChange,
handleStreamPress,
-
+
// Loading states
isLoading,
loadingStreams,
@@ -1079,19 +1080,19 @@ export const useStreamsScreen = () => {
showStillFetching,
showNoSourcesError,
hasStremioStreamProviders,
-
+
// Autoplay
isAutoplayWaiting,
autoplayTriggered,
-
+
// Scrapers
activeFetchingScrapers,
scraperLogos,
-
+
// Movie
movieLogoError,
setMovieLogoError,
-
+
// Episode
episodeImage,
effectiveEpisodeVote,
@@ -1099,7 +1100,7 @@ export const useStreamsScreen = () => {
hasIMDbRating,
tmdbEpisodeOverride,
selectedEpisode,
-
+
// Backdrop
mobileBackdropSource,
gradientColors,