diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 8331142..297bec0 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -723,6 +723,13 @@ const HeroSection: React.FC = memo(({ const shimmerOpacity = useSharedValue(0.3); const trailerOpacity = useSharedValue(0); const thumbnailOpacity = useSharedValue(1); + // Scroll-based pause/resume control + const pausedByScrollSV = useSharedValue(0); + const scrollGuardEnabledSV = useSharedValue(0); + const isPlayingSV = useSharedValue(0); + // Guards to avoid repeated auto-starts + const startedOnFocusRef = useRef(false); + const startedOnReadyRef = useRef(false); // Animation values for trailer unmute effects const actionButtonsOpacity = useSharedValue(1); @@ -753,14 +760,18 @@ const HeroSection: React.FC = memo(({ // Smooth transition: fade out thumbnail, fade in trailer thumbnailOpacity.value = withTiming(0, { duration: 500 }); trailerOpacity.value = withTiming(1, { duration: 500 }); + // Enable scroll guard after a brief delay to avoid immediate pause on entry + scrollGuardEnabledSV.value = 0; + setTimeout(() => { scrollGuardEnabledSV.value = 1; }, 1000); }, [thumbnailOpacity, trailerOpacity, trailerPreloaded]); - // Ensure trailer state is properly synchronized when trailer becomes ready + // Auto-start trailer when ready on initial entry if enabled useEffect(() => { - if (trailerReady && settings?.showTrailers && !globalTrailerPlaying) { - // Only start trailer if it's the initial load, not when returning from other screens - // This prevents auto-starting when returning from StreamsScreen - logger.info('HeroSection', 'Trailer ready but not playing - not auto-starting to prevent unwanted playback'); + if (trailerReady && settings?.showTrailers && !globalTrailerPlaying && !startedOnReadyRef.current) { + startedOnReadyRef.current = true; + logger.info('HeroSection', 'Trailer ready - auto-starting playback'); + setTrailerPlaying(true); + isPlayingSV.value = 1; } }, [trailerReady, settings?.showTrailers, globalTrailerPlaying, setTrailerPlaying]); @@ -1036,25 +1047,61 @@ const HeroSection: React.FC = memo(({ useCallback(() => { // Screen is focused - only resume trailer if it was previously playing and got interrupted logger.info('HeroSection', 'Screen focused'); - - // Don't automatically resume trailer when returning from other screens - // This prevents the trailer from starting when returning from StreamsScreen - // The trailer should only resume if the user explicitly wants it to play + // If trailers are enabled and not playing, start playback (unless scrolled past resume threshold) + if (settings?.showTrailers) { + setTimeout(() => { + try { + const y = (scrollY as any).value || 0; + const resumeThreshold = heroHeight.value * 0.4; + if (y < resumeThreshold && !startedOnFocusRef.current && isPlayingSV.value === 0) { + setTrailerPlaying(true); + isPlayingSV.value = 1; + startedOnFocusRef.current = true; + } + } catch (_e) { + if (!startedOnFocusRef.current && isPlayingSV.value === 0) { + setTrailerPlaying(true); + isPlayingSV.value = 1; + startedOnFocusRef.current = true; + } + } + }, 50); + } return () => { // Stop trailer when leaving this screen to prevent background playback/heat logger.info('HeroSection', 'Screen unfocused - stopping trailer playback'); setTrailerPlaying(false); + isPlayingSV.value = 0; + startedOnFocusRef.current = false; + startedOnReadyRef.current = false; }; - }, [setTrailerPlaying]) + }, [setTrailerPlaying, settings?.showTrailers]) ); - // Pause trailer when the hero is scrolled substantially off-screen + // Mirror playing state to shared value to use inside worklets + useEffect(() => { + isPlayingSV.value = globalTrailerPlaying ? 1 : 0; + }, [globalTrailerPlaying]); + + // Pause/resume trailer based on scroll with hysteresis and guard useDerivedValue(() => { + 'worklet'; try { - const threshold = heroHeight.value * 0.6; - if (scrollY.value > threshold) { + if (!scrollGuardEnabledSV.value) return; + const pauseThreshold = heroHeight.value * 0.7; // pause when beyond 70% + const resumeThreshold = heroHeight.value * 0.4; // resume when back within 40% + + const y = scrollY.value; + + if (y > pauseThreshold && isPlayingSV.value === 1 && pausedByScrollSV.value === 0) { + pausedByScrollSV.value = 1; runOnJS(setTrailerPlaying)(false); + isPlayingSV.value = 0; + } else if (y < resumeThreshold && pausedByScrollSV.value === 1) { + pausedByScrollSV.value = 0; + runOnJS(setTrailerPlaying)(true); + isPlayingSV.value = 1; } } catch (e) { // no-op diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 25af80a..e06de2c 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { StreamingContent } from '../services/catalogService'; import { catalogService } from '../services/catalogService'; import { stremioService } from '../services/stremioService'; @@ -134,6 +134,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({}); const [scraperStatuses, setScraperStatuses] = useState([]); const [activeFetchingScrapers, setActiveFetchingScrapers] = useState([]); + // Prevent re-initializing season selection repeatedly for the same series + const initializedSeasonRef = useRef(false); // Add hook for persistent seasons const { getSeason, saveSeason } = usePersistentSeasons(); @@ -591,12 +593,21 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setGroupedEpisodes(groupedAddonEpisodes); - // Set the first available season - const seasons = Object.keys(groupedAddonEpisodes).map(Number); - const firstSeason = Math.min(...seasons); - logger.log(`📺 Setting season ${firstSeason} as selected (${groupedAddonEpisodes[firstSeason]?.length || 0} episodes)`); - setSelectedSeason(firstSeason); - setEpisodes(groupedAddonEpisodes[firstSeason] || []); + // Determine initial season only once per series + const seasons = Object.keys(groupedAddonEpisodes).map(Number); + const firstSeason = Math.min(...seasons); + if (!initializedSeasonRef.current) { + const nextSeason = firstSeason; + if (selectedSeason !== nextSeason) { + logger.log(`📺 Setting season ${nextSeason} as selected (${groupedAddonEpisodes[nextSeason]?.length || 0} episodes)`); + setSelectedSeason(nextSeason); + } + setEpisodes(groupedAddonEpisodes[nextSeason] || []); + initializedSeasonRef.current = true; + } else { + // Keep current selection; refresh episode list for selected season + setEpisodes(groupedAddonEpisodes[selectedSeason] || []); + } // Try to get TMDB ID for additional metadata (cast, etc.) but don't override episodes const tmdbIdResult = await tmdbService.findTMDBIdByIMDB(id); @@ -640,61 +651,55 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Get the first available season as fallback const firstSeason = Math.min(...Object.keys(allEpisodes).map(Number)); - // Check for watch progress to auto-select season - let selectedSeasonNumber = firstSeason; - - try { - // Check watch progress for auto-season selection - const allProgress = await storageService.getAllWatchProgress(); - - // Find the most recently watched episode for this series - let mostRecentEpisodeId = ''; - let mostRecentTimestamp = 0; - - Object.entries(allProgress).forEach(([key, progress]) => { - if (key.includes(`series:${id}:`)) { - const episodeId = key.split(`series:${id}:`)[1]; - if (progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) { - mostRecentTimestamp = progress.lastUpdated; - mostRecentEpisodeId = episodeId; + if (!initializedSeasonRef.current) { + // Check for watch progress to auto-select season + let selectedSeasonNumber = firstSeason; + try { + const allProgress = await storageService.getAllWatchProgress(); + let mostRecentEpisodeId = ''; + let mostRecentTimestamp = 0; + Object.entries(allProgress).forEach(([key, progress]) => { + if (key.includes(`series:${id}:`)) { + const episodeId = key.split(`series:${id}:`)[1]; + if (progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) { + mostRecentTimestamp = progress.lastUpdated; + mostRecentEpisodeId = episodeId; + } } - } - }); - - if (mostRecentEpisodeId) { - // Parse season number from episode ID - const parts = mostRecentEpisodeId.split(':'); - if (parts.length === 3) { - const watchProgressSeason = parseInt(parts[1], 10); - if (transformedEpisodes[watchProgressSeason]) { - selectedSeasonNumber = watchProgressSeason; - logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for ${mostRecentEpisodeId}`); + }); + if (mostRecentEpisodeId) { + const parts = mostRecentEpisodeId.split(':'); + if (parts.length === 3) { + const watchProgressSeason = parseInt(parts[1], 10); + if (transformedEpisodes[watchProgressSeason]) { + selectedSeasonNumber = watchProgressSeason; + logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for ${mostRecentEpisodeId}`); + } + } else { + const allEpisodesList = Object.values(transformedEpisodes).flat(); + const episode = allEpisodesList.find(ep => ep.stremioId === mostRecentEpisodeId); + if (episode) { + selectedSeasonNumber = episode.season_number; + logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for episode with stremioId ${mostRecentEpisodeId}`); + } } } else { - // Try to find episode by stremioId to get season - const allEpisodesList = Object.values(transformedEpisodes).flat(); - const episode = allEpisodesList.find(ep => ep.stremioId === mostRecentEpisodeId); - if (episode) { - selectedSeasonNumber = episode.season_number; - logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for episode with stremioId ${mostRecentEpisodeId}`); - } + selectedSeasonNumber = getSeason(id, firstSeason); + logger.log(`[useMetadata] No watch progress found, using persistent season ${selectedSeasonNumber}`); } - } else { - // No watch progress found, use persistent storage as fallback + } catch (error) { + logger.error('[useMetadata] Error checking watch progress for season selection:', error); selectedSeasonNumber = getSeason(id, firstSeason); - logger.log(`[useMetadata] No watch progress found, using persistent season ${selectedSeasonNumber}`); } - } catch (error) { - logger.error('[useMetadata] Error checking watch progress for season selection:', error); - // Fall back to persistent storage - selectedSeasonNumber = getSeason(id, firstSeason); + if (selectedSeason !== selectedSeasonNumber) { + setSelectedSeason(selectedSeasonNumber); + } + setEpisodes(transformedEpisodes[selectedSeasonNumber] || []); + initializedSeasonRef.current = true; + } else { + // Keep existing selection stable and only refresh episode list for it + setEpisodes(transformedEpisodes[selectedSeason] || []); } - - // Set the selected season - setSelectedSeason(selectedSeasonNumber); - - // Set episodes for the selected season - setEpisodes(transformedEpisodes[selectedSeasonNumber] || []); } } catch (error) { if (__DEV__) console.error('Failed to load episodes:', error); @@ -1082,6 +1087,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat // Reset load attempts when id or type changes useEffect(() => { setLoadAttempts(0); + initializedSeasonRef.current = false; }, [id, type]); // Auto-retry on error with delay