unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
246 lines • 12.3 kB
JavaScript
import React, { useEffect, useState } from 'react';
export const PortraitMoreMenu = ({ video, isOpen, onClose, onDescription, onReport, onNotInterested, onCopyLink, onWishlist, onWatchLater, showDescription = true, showReport = true, showNotInterested = true, showCopyLink = true, showWishlist = true, showWatchLater = true, moreButtonRef, theme, }) => {
const wishlistColor = theme?.wishlistColor || '#ff2d55';
const watchLaterColor = theme?.watchLaterColor || '#ff2d55';
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
const [desktopPosition, setDesktopPosition] = useState({ right: 72, bottom: 120 });
useEffect(() => {
if (!moreButtonRef?.current || isMobile)
return;
const updatePosition = () => {
const button = moreButtonRef.current;
if (!button)
return;
const rect = button.getBoundingClientRect();
const gap = 12;
const right = window.innerWidth - rect.left + gap;
const menuWidth = 240;
const minRight = 16;
const maxRight = Math.max(minRight, window.innerWidth - menuWidth - 16);
const clampedRight = Math.max(minRight, Math.min(right, maxRight));
const menuApproxHeight = 280;
const buttonCenterY = rect.top + rect.height / 2;
const dropdownTop = buttonCenterY - menuApproxHeight / 2;
const bottom = window.innerHeight - dropdownTop - menuApproxHeight;
const clampedBottom = Math.max(20, Math.min(bottom, window.innerHeight - menuApproxHeight - 20));
setDesktopPosition({ right: clampedRight, bottom: clampedBottom });
};
updatePosition();
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
return () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
};
}, [moreButtonRef, isOpen, isMobile]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
if (!isOpen)
return;
const handleEscape = (e) => {
if (e.key === 'Escape')
onClose();
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);
if (!isOpen)
return null;
const iconSize = isMobile ? 24 : 20;
const options = [];
if (showDescription && onDescription) {
options.push({
icon: (React.createElement("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
React.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
React.createElement("polyline", { points: "14 2 14 8 20 8" }),
React.createElement("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
React.createElement("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
React.createElement("line", { x1: "10", y1: "9", x2: "8", y2: "9" }))),
label: 'Description',
onClick: () => {
onDescription();
onClose();
},
});
}
if (showWishlist && onWishlist) {
const inWishlist = video.isInWishlist || false;
options.push({
icon: (React.createElement("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: inWishlist ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: "2" },
React.createElement("path", { d: "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" }))),
label: inWishlist ? 'Added to Wishlist' : 'Add to Wishlist',
onClick: () => {
onWishlist(video);
onClose();
},
active: inWishlist,
activeColor: wishlistColor,
});
}
if (showWatchLater && onWatchLater) {
const inWatchLater = video.isInWatchLater || false;
options.push({
icon: (React.createElement("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
React.createElement("circle", { cx: "12", cy: "12", r: "10", fill: inWatchLater ? 'currentColor' : 'none' }),
React.createElement("polyline", { points: "12 6 12 12 16 14", stroke: inWatchLater ? '#000' : 'currentColor', strokeWidth: "2", fill: "none" }))),
label: inWatchLater ? 'Added to Watch Later' : 'Watch Later',
onClick: () => {
onWatchLater(video);
onClose();
},
active: inWatchLater,
activeColor: watchLaterColor,
});
}
if (showCopyLink && onCopyLink) {
options.push({
icon: (React.createElement("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
React.createElement("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
React.createElement("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }))),
label: 'Copy Link',
onClick: () => {
onCopyLink(video);
onClose();
},
});
}
if (showNotInterested && onNotInterested) {
options.push({
icon: (React.createElement("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
React.createElement("line", { x1: "15", y1: "9", x2: "9", y2: "15" }),
React.createElement("line", { x1: "9", y1: "9", x2: "15", y2: "15" }))),
label: 'Not Interested',
onClick: () => {
onNotInterested(video);
onClose();
},
});
}
if (showReport && onReport) {
options.push({
icon: (React.createElement("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" }))),
label: 'Report',
onClick: () => {
onReport(video);
onClose();
},
danger: true,
});
}
return (React.createElement(React.Fragment, null,
React.createElement("div", { onClick: onClose, style: {
position: 'fixed',
inset: 0,
background: isMobile ? 'rgba(0,0,0,0.6)' : 'transparent',
zIndex: 100,
opacity: isOpen ? 1 : 0,
transition: 'opacity 0.25s ease',
pointerEvents: isOpen ? 'auto' : 'none',
} }),
React.createElement("div", { style: {
position: 'fixed',
...(isMobile ? {
left: 0,
right: 0,
bottom: 0,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
transform: isOpen ? 'translateY(0)' : 'translateY(100%)',
maxHeight: '70vh',
} : {
right: desktopPosition.right,
bottom: desktopPosition.bottom,
transform: isOpen ? 'scale(1)' : 'scale(0.95)',
opacity: isOpen ? 1 : 0,
width: 240,
borderRadius: 8,
maxHeight: '50vh',
}),
background: '#1a1a1a',
zIndex: 101,
transition: isMobile ? 'transform 0.25s ease' : 'transform 0.2s ease, opacity 0.2s ease',
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
overflowY: 'auto',
border: isMobile ? 'none' : '1px solid rgba(255,255,255,0.1)',
} },
isMobile && (React.createElement("div", { style: {
display: 'flex',
justifyContent: 'center',
padding: '12px 0 8px 0',
cursor: 'grab',
} },
React.createElement("div", { style: {
width: 36,
height: 4,
borderRadius: 2,
background: 'rgba(255,255,255,0.3)',
} }))),
isMobile && (React.createElement("div", { style: {
padding: '8px 20px 12px 20px',
borderBottom: '1px solid rgba(255,255,255,0.1)',
} },
React.createElement("h3", { style: {
margin: 0,
fontSize: 16,
fontWeight: 600,
color: 'rgba(255,255,255,0.9)',
} }, "More Options"))),
React.createElement("div", { style: { padding: isMobile ? '8px 0 16px 0' : '4px 0' } }, options.map((option, index) => (React.createElement("button", { key: index, onClick: option.onClick, style: {
width: '100%',
display: 'flex',
alignItems: 'center',
gap: isMobile ? 16 : 12,
padding: isMobile ? '14px 20px' : '10px 16px',
background: 'none',
border: 'none',
color: option.danger ? '#ff3b30' : '#fff',
cursor: 'pointer',
fontSize: isMobile ? 15 : 14,
fontWeight: 500,
textAlign: 'left',
transition: 'background 0.15s ease',
WebkitTapHighlightColor: 'transparent',
}, onMouseEnter: (e) => {
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
}, onMouseLeave: (e) => {
e.currentTarget.style.background = 'none';
} },
React.createElement("div", { style: {
width: isMobile ? 24 : 20,
height: isMobile ? 24 : 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
color: option.active ? (option.activeColor || '#fff') : (option.danger ? '#ff3b30' : '#fff'),
} }, option.icon),
React.createElement("span", { style: { color: option.danger ? '#ff3b30' : '#fff' } }, option.label))))),
isMobile && (React.createElement("div", { style: {
padding: '0 20px 20px 20px',
borderTop: '1px solid rgba(255,255,255,0.1)',
} },
React.createElement("button", { onClick: onClose, style: {
width: '100%',
padding: '14px',
background: 'rgba(255,255,255,0.1)',
border: 'none',
borderRadius: 12,
color: '#fff',
fontSize: 15,
fontWeight: 600,
cursor: 'pointer',
marginTop: 12,
WebkitTapHighlightColor: 'transparent',
} }, "Cancel"))))));
};
//# sourceMappingURL=PortraitMoreMenu.js.map