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
JavaScript
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