diff --git a/app/styles/video-player.css b/app/styles/video-player.css index a5171c9..65df158 100644 --- a/app/styles/video-player.css +++ b/app/styles/video-player.css @@ -415,4 +415,12 @@ background: linear-gradient(90deg, var(--accent-color), #5ac8fa); border-radius: var(--radius-full); transition: width 0.1s linear; +} + +/* Ensure control buttons are clickable above slider touch area */ +.btn-icon { + position: relative; + z-index: 50; + cursor: pointer; + touch-action: manipulation; } \ No newline at end of file diff --git a/components/player/desktop/DesktopMoreMenu.tsx b/components/player/desktop/DesktopMoreMenu.tsx index 8e2ba36..fe34519 100644 --- a/components/player/desktop/DesktopMoreMenu.tsx +++ b/components/player/desktop/DesktopMoreMenu.tsx @@ -80,59 +80,79 @@ export function DesktopMoreMenu({ }; }, [containerRef]); - // Calculate menu position with available space awareness + // Dual Positioning Strategy const calculateMenuPosition = React.useCallback(() => { if (!buttonRef.current || !containerRef.current) return; - if (!buttonRef.current || !containerRef.current) return; + if (!isRotated) { + // Normal Mode: Non-rotated, potentially non-fullscreen + // Use Viewport Coordinates (getBoundingClientRect) and Portal to Document Body + const buttonRect = buttonRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; - // Calculate position relative to container using offsetParent loop - let top = 0; - let left = 0; - let el: HTMLElement | null = buttonRef.current; + const spaceBelow = viewportHeight - buttonRect.bottom - 10; + const spaceAbove = buttonRect.top - 10; - while (el && el !== containerRef.current) { - top += el.offsetTop; - left += el.offsetLeft; - el = el.offsetParent as HTMLElement; - } + const estimatedMenuHeight = 450; + const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; - const buttonHeight = buttonRef.current.offsetHeight; - const buttonWidth = buttonRef.current.offsetWidth; - const containerHeight = containerRef.current.offsetHeight; + const openUpward = spaceBelow < Math.min(actualMenuHeight, 300) && spaceAbove > spaceBelow; + const maxHeight = openUpward + ? Math.min(spaceAbove, actualMenuHeight) + : Math.min(spaceBelow, viewportHeight * 0.7); - // Use container dimensions for available space - const spaceBelow = containerHeight - (top + buttonHeight) - 20; - const spaceAbove = top - 20; - - // Estimate menu height (or use actual if already rendered) - const estimatedMenuHeight = 450; // approximate height of menu - const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; - - // Determine if we should open upward - const openUpward = spaceBelow < Math.min(actualMenuHeight, 300) && spaceAbove > spaceBelow; - - // Calculate max-height based on available space - const maxHeight = openUpward - ? Math.min(spaceAbove, actualMenuHeight) - : Math.min(spaceBelow, containerHeight * 0.7); - - if (openUpward) { setMenuPosition({ - top: top - 10, - left: left, + top: openUpward + ? buttonRect.top - 10 + : buttonRect.bottom + 10, + left: buttonRect.right, // Align right edge (transform handled in CSS) maxHeight: `${maxHeight}px`, - openUpward: true + openUpward: openUpward }); } else { - setMenuPosition({ - top: top + buttonHeight + 10, - left: left, - maxHeight: `${maxHeight}px`, - openUpward: false - }); + // Rotated Mode: Use Container Coordinates (offset loop) and Portal to Container + let top = 0; + let left = 0; + let el: HTMLElement | null = buttonRef.current; + + while (el && el !== containerRef.current) { + top += el.offsetTop; + left += el.offsetLeft; + el = el.offsetParent as HTMLElement; + } + + const buttonHeight = buttonRef.current.offsetHeight; + const buttonWidth = buttonRef.current.offsetWidth; + const containerHeight = containerRef.current.offsetHeight; + + const spaceBelow = containerHeight - (top + buttonHeight) - 20; + const spaceAbove = top - 20; + + const estimatedMenuHeight = 450; + const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; + + const openUpward = spaceBelow < Math.min(actualMenuHeight, 300) && spaceAbove > spaceBelow; + const maxHeight = openUpward + ? Math.min(spaceAbove, actualMenuHeight) + : Math.min(spaceBelow, containerHeight * 0.7); + + if (openUpward) { + setMenuPosition({ + top: top - 10, + left: left + buttonWidth, + maxHeight: `${maxHeight}px`, + openUpward: true + }); + } else { + setMenuPosition({ + top: top + buttonHeight + 10, + left: left + buttonWidth, + maxHeight: `${maxHeight}px`, + openUpward: false + }); + } } - }, [containerRef]); + }, [containerRef, isRotated]); @@ -154,7 +174,7 @@ export function DesktopMoreMenu({ const timer = setTimeout(calculateMenuPosition, 50); return () => clearTimeout(timer); } - }, [showMoreMenu, calculateMenuPosition]); + }, [showMoreMenu, calculateMenuPosition, isRotated]); const handleToggle = () => { if (!showMoreMenu) { @@ -166,10 +186,18 @@ export function DesktopMoreMenu({ const MenuContent = (
{/* More Menu Dropdown (Portal) */} - {showMoreMenu && containerRef.current && createPortal(MenuContent, containerRef.current)} + {/* More Menu Dropdown (Portal) */} + {showMoreMenu && typeof document !== 'undefined' && createPortal(MenuContent, (isRotated && containerRef.current) ? containerRef.current : document.body)}
); } diff --git a/components/player/desktop/DesktopSpeedMenu.tsx b/components/player/desktop/DesktopSpeedMenu.tsx index dff6357..2e14bc8 100644 --- a/components/player/desktop/DesktopSpeedMenu.tsx +++ b/components/player/desktop/DesktopSpeedMenu.tsx @@ -47,65 +47,85 @@ export function DesktopSpeedMenu({ }; }, [containerRef]); - // Calculate menu position with available space awareness + // Dual Positioning Strategy const calculateMenuPosition = React.useCallback(() => { if (!buttonRef.current || !containerRef.current) return; - if (!buttonRef.current || !containerRef.current) return; + if (!isRotated) { + // Normal Mode: Non-rotated, potentially non-fullscreen + // Use Viewport Coordinates (getBoundingClientRect) and Portal to Document Body + // This allows the menu to break out of the video container (overflow issue) + const buttonRect = buttonRef.current.getBoundingClientRect(); + // We want it to be positioned relative to the viewport + // buttonRect.top/left are already viewport coordinates + const viewportHeight = window.innerHeight; - // Calculate position relative to container using offsetParent loop - // This works regardless of container rotation because we stay in the local coordinate system - let top = 0; - let left = 0; - let el: HTMLElement | null = buttonRef.current; + const spaceBelow = viewportHeight - buttonRect.bottom - 10; + const spaceAbove = buttonRect.top - 10; - while (el && el !== containerRef.current) { - top += el.offsetTop; - left += el.offsetLeft; - el = el.offsetParent as HTMLElement; - } + const estimatedMenuHeight = 250; + const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; - const buttonHeight = buttonRef.current.offsetHeight; - const buttonWidth = buttonRef.current.offsetWidth; - const containerHeight = containerRef.current.offsetHeight; + const openUpward = spaceBelow < Math.min(actualMenuHeight, 200) && spaceAbove > spaceBelow; + const maxHeight = openUpward + ? Math.min(spaceAbove, actualMenuHeight) + : Math.min(spaceBelow, viewportHeight * 0.7); - // Use container dimensions for available space - const spaceBelow = containerHeight - (top + buttonHeight) - 20; - const spaceAbove = top - 20; - - // Estimate menu height (or use actual if already rendered) - const estimatedMenuHeight = 250; // approximate height of speed menu - const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; - - // Determine if we should open upward - const openUpward = spaceBelow < Math.min(actualMenuHeight, 200) && spaceAbove > spaceBelow; - - // Calculate max-height based on available space - const maxHeight = openUpward - ? Math.min(spaceAbove, actualMenuHeight) - : Math.min(spaceBelow, containerHeight * 0.7); - - if (openUpward) { setMenuPosition({ - top: top - 10, - left: left + buttonWidth, // Right align? No, original was left: buttonRect.right - containerRect.left - // Original logic: left = buttonRect.right - containerRect.left. - // In local coords, buttonRect.right = left + buttonWidth. - // But we want to align the RIGHT edge of menu with RIGHT edge of button? - // CSS uses `transform: translateX(-100%)` and `left: ${menuPos.left}`. - // So left should be the right edge of the button. + top: openUpward + ? buttonRect.top - 10 // Bottom of menu at top of button + : buttonRect.bottom + 10, // Top of menu at bottom of button + left: buttonRect.right, // Align right edge (transform handled in CSS) maxHeight: `${maxHeight}px`, - openUpward: true + openUpward: openUpward }); } else { - setMenuPosition({ - top: top + buttonHeight + 10, - left: left + buttonWidth, - maxHeight: `${maxHeight}px`, - openUpward: false - }); + // Rotated Mode: Fullscreen/Landscape forced + // Use Container Coordinates (offset loop) and Portal to Container + // This ensures rotation transforms apply correctly to the menu + + let top = 0; + let left = 0; + let el: HTMLElement | null = buttonRef.current; + + while (el && el !== containerRef.current) { + top += el.offsetTop; + left += el.offsetLeft; + el = el.offsetParent as HTMLElement; + } + + const buttonHeight = buttonRef.current.offsetHeight; + const buttonWidth = buttonRef.current.offsetWidth; + const containerHeight = containerRef.current.offsetHeight; + + const spaceBelow = containerHeight - (top + buttonHeight) - 20; + const spaceAbove = top - 20; + + const estimatedMenuHeight = 250; + const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; + + const openUpward = spaceBelow < Math.min(actualMenuHeight, 200) && spaceAbove > spaceBelow; + const maxHeight = openUpward + ? Math.min(spaceAbove, actualMenuHeight) + : Math.min(spaceBelow, containerHeight * 0.7); + + if (openUpward) { + setMenuPosition({ + top: top - 10, + left: left + buttonWidth, + maxHeight: `${maxHeight}px`, + openUpward: true + }); + } else { + setMenuPosition({ + top: top + buttonHeight + 10, + left: left + buttonWidth, + maxHeight: `${maxHeight}px`, + openUpward: false + }); + } } - }, [containerRef]); + }, [containerRef, isRotated]); @@ -127,7 +147,7 @@ export function DesktopSpeedMenu({ const timer = setTimeout(calculateMenuPosition, 50); return () => clearTimeout(timer); } - }, [showSpeedMenu, calculateMenuPosition]); + }, [showSpeedMenu, calculateMenuPosition, isRotated]); const handleToggle = () => { if (!showSpeedMenu) { @@ -139,10 +159,18 @@ export function DesktopSpeedMenu({ const MenuContent = (
); }