UNPKG

unified-video-framework

Version:

Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more

411 lines (402 loc) 15.8 kB
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { calculateOptimalTimeRange, getProgramsInRange } from '../utils/EPGUtils.js'; import EPGNavigationControls from './EPGNavigationControls.js'; import EPGTimelineHeader from './EPGTimelineHeader.js'; import EPGProgramGrid from './EPGProgramGrid.js'; import EPGProgramDetails from './EPGProgramDetails.js'; const DEFAULT_CONFIG = { timeSlotDuration: 60, visibleHours: 4, enableInfiniteScroll: true, lazyLoadThreshold: 200, showChannelLogos: true, showProgramImages: true, compactMode: false, showFavoriteButton: false, showRecordButton: false, showReminderButton: false, showCatchupButton: false, }; export const EPGOverlay = ({ data, config: userConfig = {}, visible = true, onToggle, className = '', style = {}, }) => { const overlayRef = useRef(null); const [updateTrigger, setUpdateTrigger] = useState(0); const [timelineScrollLeft, setTimelineScrollLeft] = useState(0); const config = useMemo(() => ({ ...DEFAULT_CONFIG, ...userConfig, }), [userConfig]); const [state, setState] = useState(() => { const currentTime = Date.now(); const { start, end } = calculateOptimalTimeRange(currentTime, config.visibleHours); return { selectedProgram: null, selectedChannel: null, timelineStart: start, timelineEnd: end, containerWidth: 2400, visibleHours: config.visibleHours, currentTime, isLoading: false, error: null, }; }); useEffect(() => { const interval = setInterval(() => { setState(prev => ({ ...prev, currentTime: Date.now(), })); }, 60000); return () => clearInterval(interval); }, []); const containerWidth = useMemo(() => { return state.visibleHours * 600; }, [state.visibleHours]); const filteredData = useMemo(() => { if (!data?.timeline) return []; return getProgramsInRange(data.timeline, state.timelineStart, state.timelineEnd, 1); }, [data?.timeline, state.timelineStart, state.timelineEnd]); const getSmartModalPosition = useCallback(() => { const basePosition = { position: 'absolute', width: '400px', maxHeight: '65%', zIndex: 200, borderRadius: '12px', overflow: 'hidden', boxShadow: '0 12px 48px rgba(0, 0, 0, 0.7)', backdropFilter: 'blur(8px)', animation: 'modalSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)', }; return { ...basePosition, top: '60px', right: '20px', '@media (max-width: 1200px)': { width: '350px', top: '40px', }, '@media (max-width: 900px)': { position: 'fixed', top: '50%', left: '50%', right: 'auto', transform: 'translate(-50%, -50%)', width: '90vw', maxWidth: '400px', maxHeight: '80vh', }, }; }, []); const handleNavigate = useCallback(async (direction) => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { let newStart; let newEnd; if (direction === 'today') { const { start, end } = calculateOptimalTimeRange(Date.now(), state.visibleHours); newStart = start; newEnd = end; } else { const timeShift = state.visibleHours * 60 * 60 * 1000; if (direction === 'left') { newStart = state.timelineStart - timeShift; newEnd = state.timelineEnd - timeShift; } else { newStart = state.timelineStart + timeShift; newEnd = state.timelineEnd + timeShift; } } setState(prev => ({ ...prev, timelineStart: newStart, timelineEnd: newEnd, containerWidth: prev.visibleHours * 600, isLoading: false, })); setUpdateTrigger(prev => prev + 1); } catch (error) { setState(prev => ({ ...prev, error: error instanceof Error ? error.message : 'Navigation failed', isLoading: false, })); } }, [state.visibleHours, state.timelineStart, state.timelineEnd]); const handleTimeRangeChange = useCallback((hours) => { const { start, end } = calculateOptimalTimeRange(state.currentTime, hours); setState(prev => ({ ...prev, visibleHours: hours, timelineStart: start, timelineEnd: end, containerWidth: hours * 600, })); setUpdateTrigger(prev => prev + 1); }, [state.currentTime]); const handleProgramSelect = useCallback((program, channel) => { setState(prev => ({ ...prev, selectedProgram: program, selectedChannel: channel, })); if (config.onProgramSelect) { config.onProgramSelect(program, channel); } }, [config]); const handleChannelSelect = useCallback((channel) => { setState(prev => ({ ...prev, selectedChannel: channel, })); if (config.onChannelSelect) { config.onChannelSelect(channel); } }, [config]); const handleAction = useCallback(async (action) => { setState(prev => ({ ...prev, isLoading: true, error: null })); try { switch (action.type) { case 'favorite': if (config.onFavorite) { await config.onFavorite(action.program, action.channel); } break; case 'record': if (config.onRecord) { await config.onRecord(action.program, action.channel); } break; case 'reminder': if (config.onSetReminder) { await config.onSetReminder(action.program, action.channel); } break; case 'catchup': if (config.onCatchup) { await config.onCatchup(action.program, action.channel); } break; } if (filteredData && state.selectedProgram) { const updatedProgram = { ...state.selectedProgram }; switch (action.type) { case 'favorite': updatedProgram.isFavorite = !updatedProgram.isFavorite; break; case 'record': updatedProgram.isRecording = !updatedProgram.isRecording; break; case 'reminder': updatedProgram.hasReminder = !updatedProgram.hasReminder; break; } setState(prev => ({ ...prev, selectedProgram: updatedProgram, isLoading: false, })); } else { setState(prev => ({ ...prev, isLoading: false })); } } catch (error) { setState(prev => ({ ...prev, error: error instanceof Error ? error.message : 'Action failed', isLoading: false, })); } }, [config, filteredData, state.selectedProgram]); const handleTimelineScroll = useCallback((scrollLeft) => { setTimelineScrollLeft(scrollLeft); }, []); const handleProgramGridScroll = useCallback((scrollLeft) => { setTimelineScrollLeft(scrollLeft); }, []); const handleTimeClick = useCallback((timestamp) => { const newStart = timestamp - (state.visibleHours * 30 * 60 * 1000); const newEnd = newStart + (state.visibleHours * 60 * 60 * 1000); setState(prev => ({ ...prev, timelineStart: newStart, timelineEnd: newEnd, })); setUpdateTrigger(prev => prev + 1); }, [state.visibleHours]); const handleCloseDetails = useCallback(() => { setState(prev => ({ ...prev, selectedProgram: null, })); }, []); useEffect(() => { const handleKeyPress = (e) => { if (!visible) return; switch (e.key) { case 'Home': e.preventDefault(); handleNavigate('today'); break; case 'Escape': if (state.selectedProgram) { e.preventDefault(); handleCloseDetails(); } break; } }; if (visible) { document.addEventListener('keydown', handleKeyPress); } return () => { document.removeEventListener('keydown', handleKeyPress); }; }, [visible, handleNavigate, handleCloseDetails, state.selectedProgram]); if (!visible) return null; const canNavigateLeft = true; const canNavigateRight = true; return (React.createElement("div", { ref: overlayRef, className: `epg-overlay ${className}`, style: { position: 'fixed', top: '35%', left: 0, right: 0, bottom: 0, backgroundColor: '#0a0a0a', color: '#fff', zIndex: 100, display: 'flex', flexDirection: 'column', overflow: 'hidden', ...style, } }, React.createElement(EPGNavigationControls, { onNavigate: handleNavigate, onTimeRangeChange: handleTimeRangeChange, canNavigateLeft: canNavigateLeft, canNavigateRight: canNavigateRight, currentTime: state.currentTime, timelineStart: state.timelineStart, timelineEnd: state.timelineEnd, visibleHours: state.visibleHours }), React.createElement(EPGTimelineHeader, { timelineStart: state.timelineStart, timelineEnd: state.timelineEnd, containerWidth: containerWidth, currentTime: state.currentTime, visibleHours: state.visibleHours, slotDuration: config.timeSlotDuration, onTimeClick: handleTimeClick, scrollLeft: timelineScrollLeft, onScroll: handleTimelineScroll }), React.createElement("div", { className: "epg-main-content", style: { flex: 1, display: 'flex', overflow: 'hidden', position: 'relative', } }, React.createElement(EPGProgramGrid, { data: filteredData, timelineStart: state.timelineStart, timelineEnd: state.timelineEnd, containerWidth: containerWidth, currentTime: state.currentTime, selectedProgram: state.selectedProgram, onProgramSelect: handleProgramSelect, onChannelSelect: handleChannelSelect, onTimelineScroll: handleProgramGridScroll, timelineScrollLeft: timelineScrollLeft, channelHeight: 80, visibleChannels: 6, style: { flex: 1 } }), state.selectedProgram && (React.createElement("div", { className: "epg-details-panel", style: getSmartModalPosition() }, React.createElement(EPGProgramDetails, { program: state.selectedProgram, channel: state.selectedChannel || undefined, onClose: handleCloseDetails, onAction: handleAction, isModal: false, currentTime: state.currentTime })))), state.isLoading && (React.createElement("div", { className: "epg-loading-overlay", style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, } }, React.createElement("div", { style: { color: '#fff', fontSize: '18px', fontWeight: '600', } }, "Loading..."))), state.error && (React.createElement("div", { className: "epg-error-message", style: { position: 'absolute', top: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: '#e74c3c', color: '#fff', padding: '12px 20px', borderRadius: '8px', fontSize: '14px', fontWeight: '600', zIndex: 400, maxWidth: '400px', textAlign: 'center', boxShadow: '0 4px 12px rgba(231, 76, 60, 0.3)', } }, state.error, React.createElement("button", { onClick: () => setState(prev => ({ ...prev, error: null })), style: { marginLeft: '12px', backgroundColor: 'transparent', border: 'none', color: '#fff', cursor: 'pointer', fontSize: '16px', padding: '0', } }, "\u00D7"))), React.createElement("style", null, ` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes modalSlideIn { from { transform: translateY(-30px); opacity: 0; scale: 0.9; } to { transform: translateY(0); opacity: 1; scale: 1; } } .epg-overlay * { box-sizing: border-box; } .epg-overlay::-webkit-scrollbar { width: 8px; height: 8px; } .epg-overlay::-webkit-scrollbar-track { background: #1a1a1a; } .epg-overlay::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; } .epg-overlay::-webkit-scrollbar-thumb:hover { background: #666; } /* Enhanced modal styling */ .epg-details-panel { backdrop-filter: blur(16px); background: linear-gradient(135deg, rgba(26, 26, 26, 0.95), rgba(42, 42, 42, 0.95)); border: 1px solid rgba(255, 255, 255, 0.1); } /* Responsive modal positioning */ @media (max-width: 1200px) { .epg-details-panel { width: 350px !important; top: 40px !important; } } @media (max-width: 900px) { .epg-details-panel { position: fixed !important; top: 50% !important; left: 50% !important; right: auto !important; transform: translate(-50%, -50%) !important; width: 90vw !important; max-width: 400px !important; max-height: 80vh !important; } } `))); }; export default EPGOverlay; //# sourceMappingURL=EPGOverlay-improved-positioning.js.map