unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
265 lines • 14.3 kB
JavaScript
import React, { useMemo, useRef, useEffect, useState, useCallback } from 'react';
import { calculateProgramBlock, isProgramLive, getProgramProgress, formatTime, throttle } from "../utils/EPGUtils.js";
const ProgramBlockComponent = ({ block, isSelected, isLive, progress, onClick, channelHeight, }) => {
const [isHovered, setIsHovered] = useState(false);
return (React.createElement("div", { className: `epg-program-block ${isSelected ? 'selected' : ''} ${isLive ? 'live' : ''}`, style: {
position: 'absolute',
left: `${block.left}px`,
width: `${block.width}px`,
height: `${channelHeight - 4}px`,
top: '2px',
backgroundColor: isLive ? '#ff6b35' : isSelected ? '#4a90e2' : 'rgba(42, 42, 42, 0.8)',
border: `${isHovered ? '2px' : '1px'} solid ${isSelected ? '#4a90e2' : isHovered ? '#fff' : isLive ? '#ff6b35' : 'rgba(255, 255, 255, 0.15)'}`,
borderRadius: '6px',
cursor: 'pointer',
overflow: 'hidden',
transition: 'all 200ms ease',
zIndex: isSelected ? 10 : isHovered ? 8 : isLive ? 5 : 1,
boxShadow: isSelected ? '0 0 20px rgba(74, 144, 226, 0.4)' : isHovered ? '0 4px 12px rgba(255, 255, 255, 0.1)' : '0 2px 4px rgba(0,0,0,0.3)',
transform: isHovered ? 'scale(1.02)' : 'scale(1)',
}, onClick: onClick, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false) },
isLive && progress > 0 && (React.createElement("div", { className: "epg-program-progress", style: {
position: 'absolute',
top: '0',
left: '0',
height: '2px',
width: `${progress}%`,
backgroundColor: '#fff',
transition: 'width 0.3s ease',
} })),
React.createElement("div", { style: {
padding: '4px 8px',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
color: isLive || isSelected ? '#fff' : '#e0e0e0',
} },
React.createElement("div", { className: "epg-program-title", style: {
fontSize: block.width > 120 ? '13px' : '11px',
fontWeight: '600',
lineHeight: '1.2',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginBottom: '2px',
} }, block.program.title),
block.width > 80 && (React.createElement("div", { className: "epg-program-time", style: {
fontSize: '10px',
opacity: 0.8,
lineHeight: '1.2',
} },
formatTime(block.start),
" - ",
formatTime(block.end))),
block.width > 160 && (block.program.category || block.program.rating) && (React.createElement("div", { className: "epg-program-meta", style: {
fontSize: '9px',
opacity: 0.7,
marginTop: 'auto',
display: 'flex',
gap: '4px',
} },
block.program.category && (React.createElement("span", { className: "category" }, block.program.category)),
block.program.rating && (React.createElement("span", { className: "rating" }, block.program.rating)))),
isLive && (React.createElement("div", { className: "epg-live-indicator", style: {
position: 'absolute',
top: '4px',
right: '4px',
fontSize: '8px',
fontWeight: '700',
color: '#fff',
backgroundColor: 'rgba(255, 255, 255, 0.2)',
padding: '1px 4px',
borderRadius: '2px',
letterSpacing: '0.5px',
} }, "LIVE")))));
};
export const EPGProgramGrid = ({ data, timelineStart, timelineEnd, containerWidth, currentTime = Date.now(), selectedProgram, onProgramSelect, onChannelSelect, onTimelineScroll, timelineScrollLeft = 0, channelHeight = 80, visibleChannels = 6, className = '', style = {}, }) => {
const gridRef = useRef(null);
const channelNamesRef = useRef(null);
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const programBlocks = useMemo(() => {
const blocks = new Map();
data.forEach(channel => {
const channelBlocks = [];
channel.data.forEach(program => {
const block = calculateProgramBlock(program, channel, timelineStart, timelineEnd, containerWidth, channelHeight);
if (block) {
channelBlocks.push(block);
}
});
blocks.set(channel.programTitle, channelBlocks);
});
return blocks;
}, [data, timelineStart, timelineEnd, containerWidth, channelHeight]);
const handleProgramSelect = useCallback((program, channel) => {
if (onProgramSelect) {
onProgramSelect(program, channel);
}
}, [onProgramSelect]);
const handleChannelSelect = useCallback((channel) => {
if (onChannelSelect) {
onChannelSelect(channel);
}
}, [onChannelSelect]);
const handleScroll = useMemo(() => throttle((e) => {
const target = e.currentTarget;
setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft);
if (onTimelineScroll) {
onTimelineScroll(target.scrollLeft);
}
}, 16), [onTimelineScroll]);
useEffect(() => {
if (channelNamesRef.current) {
const channelNamesContainer = channelNamesRef.current.querySelector('.epg-channel-names-content');
if (channelNamesContainer) {
channelNamesContainer.style.transform = `translateY(-${scrollTop}px)`;
}
}
}, [scrollTop]);
useEffect(() => {
if (gridRef.current && timelineScrollLeft !== undefined) {
gridRef.current.scrollLeft = timelineScrollLeft;
}
}, [timelineScrollLeft]);
const visibleChannelRange = useMemo(() => {
const startIndex = Math.floor(scrollTop / channelHeight);
const endIndex = Math.min(startIndex + visibleChannels + 2, data.length);
return { startIndex, endIndex };
}, [scrollTop, channelHeight, visibleChannels, data.length]);
return (React.createElement("div", { className: `epg-program-grid ${className}`, style: {
position: 'relative',
height: '100%',
overflow: 'hidden',
backgroundColor: 'transparent',
...style,
} },
React.createElement("div", { ref: channelNamesRef, className: "epg-channel-names", style: {
position: 'absolute',
left: '0',
top: '0',
bottom: '0',
width: '200px',
backgroundColor: 'rgba(20, 20, 20, 0.6)',
borderRight: '1px solid rgba(255, 255, 255, 0.1)',
zIndex: 10,
overflow: 'hidden',
backdropFilter: 'blur(8px)',
} },
React.createElement("div", { className: "epg-channel-names-content", style: {
height: `${data.length * channelHeight}px`,
position: 'relative',
} }, data.slice(visibleChannelRange.startIndex, visibleChannelRange.endIndex).map((channel, index) => {
const actualIndex = visibleChannelRange.startIndex + index;
return (React.createElement("div", { key: channel.programTitle, className: "epg-channel-name", style: {
position: 'absolute',
top: `${actualIndex * channelHeight}px`,
left: '0',
right: '0',
height: `${channelHeight}px`,
display: 'flex',
alignItems: 'center',
padding: '0 12px',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
cursor: 'pointer',
backgroundColor: 'rgba(20, 20, 20, 0.3)',
transition: 'all 200ms ease',
backdropFilter: 'blur(6px)',
}, onClick: () => handleChannelSelect(channel), onMouseEnter: (e) => {
e.currentTarget.style.backgroundColor = 'rgba(20, 20, 20, 0.5)';
}, onMouseLeave: (e) => {
e.currentTarget.style.backgroundColor = 'rgba(20, 20, 20, 0.3)';
} },
channel.channelLogo && (React.createElement("img", { src: channel.channelLogo, alt: channel.programTitle, style: {
width: '32px',
height: '32px',
borderRadius: '4px',
marginRight: '8px',
objectFit: 'cover',
}, onError: (e) => {
e.currentTarget.style.display = 'none';
} })),
React.createElement("div", { style: {
color: '#fff',
fontSize: '14px',
fontWeight: '600',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
} }, channel.programTitle)));
}))),
React.createElement("div", { ref: gridRef, className: "epg-programs-container", style: {
position: 'absolute',
left: '200px',
top: '0',
right: '0',
bottom: '0',
overflow: 'auto',
}, onScroll: handleScroll },
React.createElement("div", { className: "epg-programs-grid", style: {
position: 'relative',
width: `${containerWidth}px`,
height: `${data.length * channelHeight}px`,
} },
data.map((_, index) => (React.createElement("div", { key: `h-grid-line-${index}`, style: {
position: 'absolute',
top: `${(index + 1) * channelHeight}px`,
left: '0',
right: '0',
height: '1px',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
opacity: 1,
} }))),
(() => {
const lines = [];
const timeRange = timelineEnd - timelineStart;
const slotDuration = 60 * 60 * 1000;
const numSlots = Math.ceil(timeRange / slotDuration);
for (let i = 0; i <= numSlots; i++) {
const lineTime = timelineStart + (i * slotDuration);
const linePosition = ((lineTime - timelineStart) / timeRange) * containerWidth;
lines.push(React.createElement("div", { key: `v-grid-line-${i}`, style: {
position: 'absolute',
left: `${linePosition}px`,
top: '0',
bottom: '0',
width: '1px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
opacity: 1,
} }));
}
return lines;
})(),
data.slice(visibleChannelRange.startIndex, visibleChannelRange.endIndex).map((channel, index) => {
const actualIndex = visibleChannelRange.startIndex + index;
const channelBlocks = programBlocks.get(channel.programTitle) || [];
return (React.createElement("div", { key: channel.programTitle, className: "epg-channel-programs", style: {
position: 'absolute',
top: `${actualIndex * channelHeight}px`,
left: '0',
right: '0',
height: `${channelHeight}px`,
} }, channelBlocks.map((block, blockIndex) => {
const isSelected = selectedProgram?.id === block.program.id;
const isLive = isProgramLive(block.program, currentTime);
const progress = isLive ? getProgramProgress(block.program, currentTime) : 0;
return (React.createElement(ProgramBlockComponent, { key: `${block.program.id}-${blockIndex}`, block: block, isSelected: isSelected, isLive: isLive, progress: progress, channelHeight: channelHeight, onClick: () => handleProgramSelect(block.program, channel) }));
})));
}),
currentTime >= timelineStart && currentTime <= timelineEnd && (React.createElement("div", { className: "epg-current-time-line", style: {
position: 'absolute',
left: `${((currentTime - timelineStart) / (timelineEnd - timelineStart)) * containerWidth}px`,
top: '0',
bottom: '0',
width: '2px',
backgroundColor: '#ff6b35',
zIndex: 20,
pointerEvents: 'none',
boxShadow: '0 0 4px rgba(255, 107, 53, 0.5)',
} }))))));
};
export default EPGProgramGrid;
//# sourceMappingURL=EPGProgramGrid.js.map