UNPKG

unified-video-framework

Version:

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

225 lines 11.4 kB
import React, { useEffect, useRef, useState } from 'react'; function formatDuration(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } export default function PlaylistPanel({ title, items, currentIndex, loop, shuffle, visible, inline = false, position = 'right', width = 380, isFullscreen = false, onClose, onItemClick, onLoopToggle, onShuffleToggle, }) { const activeItemRef = useRef(null); useEffect(() => { if (activeItemRef.current && visible) { activeItemRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } }, [currentIndex, visible]); const [isSmall, setIsSmall] = useState(() => typeof window !== 'undefined' && window.innerWidth < 640); useEffect(() => { const check = () => setIsSmall(window.innerWidth < 640); window.addEventListener('resize', check); return () => window.removeEventListener('resize', check); }, []); const mutedColor = 'rgba(255,255,255,0.4)'; const commonStyle = { background: 'rgba(18,18,18,0.96)', backdropFilter: 'blur(8px)', display: 'flex', flexDirection: 'column', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }; const panelStyle = inline ? { ...commonStyle, position: 'relative', width, flexShrink: 0, height: '100%', borderLeft: '1px solid rgba(255,255,255,0.08)', zIndex: isFullscreen ? 2147483647 : 10, } : isSmall ? { ...commonStyle, position: 'fixed', bottom: 0, left: 0, right: 0, width: '100%', height: '50vh', borderTop: '1px solid rgba(255,255,255,0.1)', transform: visible ? 'translateY(0)' : 'translateY(100%)', transition: 'transform 240ms ease', zIndex: isFullscreen ? 2147483647 : 1100, pointerEvents: visible ? 'auto' : 'none', paddingBottom: 'env(safe-area-inset-bottom)', } : { ...commonStyle, position: 'fixed', top: 0, bottom: 0, right: position === 'right' ? 0 : undefined, left: position === 'left' ? 0 : undefined, width, transform: visible ? 'translateX(0)' : position === 'right' ? 'translateX(100%)' : 'translateX(-100%)', transition: 'transform 240ms ease', zIndex: isFullscreen ? 2147483647 : 1100, pointerEvents: visible ? 'auto' : 'none', }; const iconBtnStyle = (active) => ({ background: 'transparent', border: 'none', cursor: 'pointer', padding: '4px', color: active ? '#fff' : mutedColor, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 4, transition: 'color 0.15s', }); return (React.createElement("aside", { style: panelStyle, "aria-hidden": !visible, "aria-label": "Playlist" }, isSmall && (React.createElement("div", { style: { width: 36, height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.2)', margin: '8px auto 0', flexShrink: 0, } })), React.createElement("div", { style: { padding: '12px 16px 8px', borderBottom: '1px solid rgba(255,255,255,0.1)', flexShrink: 0, } }, React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 4 } }, title ? (React.createElement("div", { style: { color: '#fff', fontWeight: 700, fontSize: 14, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', } }, title)) : (React.createElement("div", { style: { flex: 1, color: '#fff', fontWeight: 700, fontSize: 14 } }, "Playlist")), React.createElement("button", { onClick: onLoopToggle, title: "Loop playlist", "aria-label": "Toggle loop", style: iconBtnStyle(loop) }, React.createElement("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "currentColor" }, React.createElement("path", { d: "M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" }))), React.createElement("button", { onClick: onShuffleToggle, title: "Shuffle playlist", "aria-label": "Toggle shuffle", style: iconBtnStyle(shuffle) }, React.createElement("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "currentColor" }, React.createElement("path", { d: "M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z" }))), React.createElement("button", { onClick: onClose, title: "Close playlist", "aria-label": "Close playlist", style: { ...iconBtnStyle(false), color: '#fff', fontSize: 20, lineHeight: '1', } }, "\u00D7")), React.createElement("div", { style: { color: 'rgba(255,255,255,0.45)', fontSize: 11, marginTop: 4 } }, currentIndex + 1, " / ", items.length)), React.createElement("ul", { style: { listStyle: 'none', margin: 0, padding: 0, overflowY: 'auto', flex: 1, scrollbarWidth: 'thin', scrollbarColor: 'rgba(255,255,255,0.2) transparent', } }, items.map((item, index) => { const isActive = index === currentIndex; return (React.createElement("li", { key: item.id, ref: isActive ? activeItemRef : null, onClick: () => onItemClick(index), style: { display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', cursor: 'pointer', background: isActive ? 'rgba(255,255,255,0.08)' : 'transparent', transition: 'background 0.15s ease', }, onMouseEnter: (e) => { if (!isActive) { e.currentTarget.style.background = 'rgba(255,255,255,0.05)'; } }, onMouseLeave: (e) => { if (!isActive) { e.currentTarget.style.background = 'transparent'; } } }, React.createElement("div", { style: { width: 20, textAlign: 'center', flexShrink: 0, color: isActive ? '#fff' : 'rgba(255,255,255,0.4)', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', } }, isActive ? (React.createElement("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "currentColor" }, React.createElement("path", { d: "M8 5v14l11-7z" }))) : (React.createElement("span", null, index + 1))), React.createElement("div", { style: { position: 'relative', flexShrink: 0, width: 96, height: 54 } }, item.thumbnail ? (React.createElement("img", { src: item.thumbnail, alt: "", width: 96, height: 54, style: { width: 96, height: 54, objectFit: 'cover', borderRadius: 4, display: 'block', } })) : (React.createElement("div", { style: { width: 96, height: 54, borderRadius: 4, background: '#2a2a2a', display: 'flex', alignItems: 'center', justifyContent: 'center', } }, React.createElement("svg", { viewBox: "0 0 24 24", width: "24", height: "24", fill: "rgba(255,255,255,0.25)" }, React.createElement("path", { d: "M8 5v14l11-7z" })))), item.duration != null && (React.createElement("div", { style: { position: 'absolute', bottom: 3, right: 3, background: 'rgba(0,0,0,0.8)', color: '#fff', fontSize: 10, padding: '1px 4px', borderRadius: 2, fontWeight: 600, lineHeight: '14px', userSelect: 'none', } }, formatDuration(item.duration)))), React.createElement("div", { style: { flex: 1, minWidth: 0 } }, React.createElement("div", { style: { color: isActive ? '#fff' : 'rgba(255,255,255,0.85)', fontSize: 13, fontWeight: isActive ? 600 : 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: '1.4', } }, item.title), item.subtitle && (React.createElement("div", { style: { color: 'rgba(255,255,255,0.4)', fontSize: 11, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', } }, item.subtitle))), React.createElement("button", { onClick: (e) => e.stopPropagation(), title: "More options", "aria-label": "More options", style: { background: 'transparent', border: 'none', cursor: 'pointer', padding: '4px 2px', color: 'rgba(255,255,255,0.35)', fontSize: 16, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', lineHeight: '1', } }, "\u22EE"))); })))); } //# sourceMappingURL=PlaylistPanel.js.map