import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Text, TouchableOpacity, StyleSheet, Platform } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSpring, Easing, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialIcons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import { introService, SkipInterval, SkipType } from '../../../services/introService'; import { useTheme } from '../../../contexts/ThemeContext'; import { logger } from '../../../utils/logger'; interface SkipIntroButtonProps { imdbId: string | undefined; type: 'movie' | 'series' | string; season?: number; episode?: number; malId?: string; kitsuId?: string; currentTime: number; onSkip: (endTime: number) => void; controlsVisible?: boolean; controlsFixedOffset?: number; } export const SkipIntroButton: React.FC = ({ imdbId, type, season, episode, malId, kitsuId, currentTime, onSkip, controlsVisible = false, controlsFixedOffset = 100, }) => { const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); // State const [skipIntervals, setSkipIntervals] = useState([]); const [currentInterval, setCurrentInterval] = useState(null); const [isVisible, setIsVisible] = useState(false); const [hasSkippedCurrent, setHasSkippedCurrent] = useState(false); const [autoHidden, setAutoHidden] = useState(false); // Refs const fetchedRef = useRef(false); const lastEpisodeRef = useRef(''); const autoHideTimerRef = useRef(null); // Animation values const opacity = useSharedValue(0); const scale = useSharedValue(0.8); const translateY = useSharedValue(0); // Fetch skip data when episode changes useEffect(() => { const episodeKey = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}`; // Skip if not a series or missing required data (though MAL/Kitsu ID might be enough for some cases, usually need season/ep) if (type !== 'series' || (!imdbId && !malId && !kitsuId) || !season || !episode) { setSkipIntervals([]); fetchedRef.current = false; return; } // Skip if already fetched for this episode if (lastEpisodeRef.current === episodeKey && fetchedRef.current) { return; } lastEpisodeRef.current = episodeKey; fetchedRef.current = true; setHasSkippedCurrent(false); setAutoHidden(false); setSkipIntervals([]); const fetchSkipData = async () => { logger.log(`[SkipIntroButton] Fetching skip data for S${season}E${episode} (IMDB: ${imdbId}, MAL: ${malId}, Kitsu: ${kitsuId})...`); try { const intervals = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId); setSkipIntervals(intervals); if (intervals.length > 0) { logger.log(`[SkipIntroButton] ✓ Found ${intervals.length} skip intervals:`, intervals); } else { logger.log(`[SkipIntroButton] ✗ No skip data available for this episode`); } } catch (error) { logger.error('[SkipIntroButton] Error fetching skip data:', error); setSkipIntervals([]); } }; fetchSkipData(); }, [imdbId, type, season, episode, malId, kitsuId]); // Determine active interval based on current playback position useEffect(() => { if (skipIntervals.length === 0) { setCurrentInterval(null); return; } // Find an interval that contains the current time const active = skipIntervals.find( interval => currentTime >= interval.startTime && currentTime < (interval.endTime - 0.5) ); if (active) { // If we found a new active interval that is different from the previous one if (!currentInterval || active.startTime !== currentInterval.startTime || active.type !== currentInterval.type) { logger.log(`[SkipIntroButton] Entering interval: ${active.type} (${active.startTime}-${active.endTime})`); setCurrentInterval(active); setHasSkippedCurrent(false); // Reset skipped state for new interval setAutoHidden(false); // Reset auto-hide for new interval } } else { // No active interval if (currentInterval) { logger.log('[SkipIntroButton] Exiting interval'); setCurrentInterval(null); } } }, [currentTime, skipIntervals]); // Determine if button should show const shouldShowButton = useCallback(() => { if (!currentInterval || hasSkippedCurrent) return false; // If auto-hidden, only show when controls are visible if (autoHidden && !controlsVisible) return false; return true; }, [currentInterval, hasSkippedCurrent, autoHidden, controlsVisible]); // Handle visibility animations useEffect(() => { const shouldShow = shouldShowButton(); if (shouldShow && !isVisible) { setIsVisible(true); opacity.value = withTiming(1, { duration: 300, easing: Easing.out(Easing.cubic) }); scale.value = withSpring(1, { damping: 15, stiffness: 150 }); // Start 15-second auto-hide timer if (autoHideTimerRef.current) clearTimeout(autoHideTimerRef.current); autoHideTimerRef.current = setTimeout(() => { if (!hasSkippedCurrent) { setAutoHidden(true); opacity.value = withTiming(0, { duration: 200 }); scale.value = withTiming(0.8, { duration: 200 }); setTimeout(() => setIsVisible(false), 250); } }, 15000); } else if (!shouldShow && isVisible) { if (autoHideTimerRef.current) clearTimeout(autoHideTimerRef.current); opacity.value = withTiming(0, { duration: 200 }); scale.value = withTiming(0.8, { duration: 200 }); // Delay hiding to allow animation to complete setTimeout(() => setIsVisible(false), 250); } }, [shouldShowButton, isVisible, hasSkippedCurrent]); // Re-show when controls become visible (if still in interval and was auto-hidden) useEffect(() => { if (controlsVisible && autoHidden && currentInterval && !hasSkippedCurrent) { setAutoHidden(false); } }, [controlsVisible, autoHidden, currentInterval, hasSkippedCurrent]); // Cleanup timer on unmount useEffect(() => { return () => { if (autoHideTimerRef.current) clearTimeout(autoHideTimerRef.current); }; }, []); // Animate position based on controls visibility useEffect(() => { // Android needs more offset to clear the slider const androidOffset = controlsFixedOffset - 8; const iosOffset = controlsFixedOffset / 2; const target = controlsVisible ? -(Platform.OS === 'android' ? androidOffset : iosOffset) : 0; translateY.value = withTiming(target, { duration: 220, easing: Easing.out(Easing.cubic) }); }, [controlsVisible, controlsFixedOffset]); // Handle skip action const handleSkip = useCallback(() => { if (!currentInterval) return; logger.log(`[SkipIntroButton] User pressed Skip - seeking to ${currentInterval.endTime}s`); setHasSkippedCurrent(true); onSkip(currentInterval.endTime); }, [currentInterval, onSkip]); // Get display text based on skip type const getButtonText = () => { if (!currentInterval) return 'Skip'; switch (currentInterval.type) { case 'op': case 'mixed-op': case 'intro': return 'Skip Intro'; case 'ed': case 'mixed-ed': case 'outro': return 'Skip Ending'; case 'recap': return 'Skip Recap'; default: return 'Skip'; } }; // Animated styles const containerStyle = useAnimatedStyle(() => ({ opacity: opacity.value, transform: [{ scale: scale.value }, { translateY: translateY.value }], })); // Don't render if not visible (and animation complete) if (!isVisible && opacity.value === 0) { return null; } return ( {getButtonText()} ); }; const styles = StyleSheet.create({ container: { position: 'absolute', zIndex: 55, }, button: { borderRadius: 12, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 8, }, blurContainer: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 18, backgroundColor: 'rgba(30, 30, 30, 0.7)', }, icon: { marginRight: 8, }, text: { color: '#FFFFFF', fontSize: 14, fontWeight: '600', letterSpacing: 0.3, }, accentBar: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, }, }); export default SkipIntroButton;