'use client'; import React from 'react'; import { Icons } from '@/components/ui/Icon'; import { usePlayerSettings } from '../hooks/usePlayerSettings'; import { settingsStore, AdFilterMode } from '@/lib/store/settings-store'; import { createPortal } from 'react-dom'; interface DesktopMoreMenuProps { showMoreMenu: boolean; isProxied?: boolean; onToggleMoreMenu: () => void; onMouseEnter: () => void; onMouseLeave: () => void; onCopyLink: (type?: 'original' | 'proxy') => void; containerRef: React.RefObject; isRotated?: boolean; } export function DesktopMoreMenu({ showMoreMenu, isProxied = false, onToggleMoreMenu, onMouseEnter, onMouseLeave, onCopyLink, containerRef, isRotated = false }: DesktopMoreMenuProps) { const { autoNextEpisode, autoSkipIntro, skipIntroSeconds, autoSkipOutro, skipOutroSeconds, showModeIndicator, adFilter, setAutoNextEpisode, setAutoSkipIntro, setSkipIntroSeconds, setAutoSkipOutro, setSkipOutroSeconds, setShowModeIndicator, setAdFilter, adFilterMode, setAdFilterMode, fullscreenType, setFullscreenType, } = usePlayerSettings(); const buttonRef = React.useRef(null); const menuRef = React.useRef(null); const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0, maxHeight: 'none', openUpward: false, align: 'right' as 'left' | 'right' }); const [isAdFilterOpen, setAdFilterOpen] = React.useState(false); const AD_FILTER_LABELS: Record = { off: '关闭', keyword: '关键词', heuristic: '智能(Beta)', aggressive: '激进' }; const [isFullscreen, setIsFullscreen] = React.useState(false); React.useEffect(() => { const updateFullscreen = () => { // Check both native fullscreen and window fullscreen (CSS-based) const nativeFullscreen = !!document.fullscreenElement; const windowFullscreen = containerRef.current?.closest('.is-web-fullscreen') !== null; setIsFullscreen(nativeFullscreen || windowFullscreen); }; document.addEventListener('fullscreenchange', updateFullscreen); // Also check periodically for window fullscreen changes (CSS class based) const interval = setInterval(updateFullscreen, 500); updateFullscreen(); return () => { document.removeEventListener('fullscreenchange', updateFullscreen); clearInterval(interval); }; }, [containerRef]); // Dual Positioning Strategy const calculateMenuPosition = React.useCallback(() => { if (!buttonRef.current || !containerRef.current) return; if (!isRotated) { // Normal Mode: Non-rotated (Portrait on Mobile) // Use Viewport Coordinates but position relative to button (User Request: "Below button") // And use Body Portal to escape container clipping const buttonRect = buttonRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const spaceBelow = viewportHeight - buttonRect.bottom - 10; const spaceAbove = buttonRect.top - 10; 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, viewportHeight * 0.7); // Smart Horizontal Alignment: // If button is on the left half of screen, align menu's left edge to button's left. // If button is on the right half of screen, align menu's right edge to button's right. const isLeftHalf = buttonRect.left < viewportWidth / 2; const align = isLeftHalf ? 'left' : 'right'; let left = isLeftHalf ? buttonRect.left : buttonRect.right; // Boundary clamping if (isLeftHalf) { left = Math.max(left, 10); } else { left = Math.min(left, viewportWidth - 10); } setMenuPosition({ top: openUpward ? buttonRect.top - 10 : buttonRect.bottom + 10, left: left, maxHeight: `${maxHeight}px`, openUpward: openUpward, align: align }); } else { // 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 buttonWidth = buttonRef.current.offsetWidth; const buttonHeight = buttonRef.current.offsetHeight; const containerWidth = containerRef.current.offsetWidth; const containerHeight = containerRef.current.offsetHeight; // In rotated mode (90deg CW): // Landscape Vertical Axis = Container X Axis (left in code) // Landscape Horizontal Axis = Container Y Axis (top in code) // Visual available space in landscape: // Top of landscape is Container Left (x=0) // Bottom of landscape is Container Right (x=H_cont) const spaceToLandscapeTop = left; const spaceToLandscapeBottom = containerWidth - (left + buttonWidth); const estimatedMenuHeight = 450; const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight; const openUpward = spaceToLandscapeBottom < Math.min(actualMenuHeight, 300) && spaceToLandscapeTop > spaceToLandscapeBottom; const maxHeight = openUpward ? Math.min(spaceToLandscapeTop - 10, actualMenuHeight) : Math.min(spaceToLandscapeBottom - 10, containerHeight * 0.7); // Horizontal alignment (Landscape): // Landscape Right = Container Top (y=0) // Landscape Left = Container Bottom (y=H_cont) const isLandscapeRightHalf = top < containerHeight / 2; const align = isLandscapeRightHalf ? 'left' : 'right'; setMenuPosition({ top: top, // Fixed horizontal container coordinate left: left, // Fixed vertical container coordinate maxHeight: `${maxHeight}px`, openUpward: openUpward, align: align }); } }, [containerRef, isRotated]); // Auto-close menu on scroll React.useEffect(() => { if (!showMoreMenu) return; const handleScroll = () => { if (showMoreMenu) { onToggleMoreMenu(); } }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, [showMoreMenu, onToggleMoreMenu]); React.useEffect(() => { if (showMoreMenu) { calculateMenuPosition(); const timer = setTimeout(calculateMenuPosition, 50); return () => clearTimeout(timer); } }, [showMoreMenu, calculateMenuPosition, isRotated]); const handleToggle = () => { if (!showMoreMenu) { calculateMenuPosition(); } onToggleMoreMenu(); }; const MenuContent = (
e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} > {/* Copy Link Options */} {isProxied ? ( <> ) : ( )} {/* Divider */}
{/* Fullscreen Mode Selector */}
全屏方式
{/* Show Mode Indicator Switch */}
模式指示器
{/* Ad Filter Mode Selector */}
广告过滤
{/* Custom Ad Filter Mode Selector */}
{isAdFilterOpen && ( <>
setAdFilterOpen(false)} />
{Object.entries(AD_FILTER_LABELS).map(([mode, label]) => ( ))}
)}
{/* Auto Next Episode Switch */}
自动下一集
{/* Skip Intro Switch */}
跳过片头
{/* Expandable Input */} {autoSkipIntro && (
时长: setSkipIntroSeconds(parseInt(e.target.value) || 0)} className={`text-center bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] text-[var(--text-color)] focus:outline-none focus:border-[var(--accent-color)] no-spinner ${isRotated ? 'w-10 px-1 py-0 text-[10px]' : 'w-12 sm:w-16 px-1.5 py-0.5 sm:px-2 sm:py-1 text-xs sm:text-sm'}`} onClick={(e) => e.stopPropagation()} />
)}
{/* Skip Outro Switch */}
跳过片尾
{/* Expandable Input */} {autoSkipOutro && (
剩余: setSkipOutroSeconds(parseInt(e.target.value) || 0)} className={`text-center bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] text-[var(--text-color)] focus:outline-none focus:border-[var(--accent-color)] no-spinner ${isRotated ? 'w-10 px-1 py-0 text-[10px]' : 'w-12 sm:w-16 px-1.5 py-0.5 sm:px-2 sm:py-1 text-xs sm:text-sm'}`} onClick={(e) => e.stopPropagation()} />
)}
); return (
{/* More Menu Dropdown (Portal) */} {/* More Menu Dropdown (Portal) */} {showMoreMenu && typeof document !== 'undefined' && createPortal(MenuContent, (isRotated && containerRef.current) ? containerRef.current : document.body)}
); }