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