'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; isPremium?: boolean; isProxied?: boolean; onToggleMoreMenu: () => void; onMouseEnter: () => void; onMouseLeave: () => void; onCopyLink: (type?: 'original' | 'proxy') => void; webFullscreenSize: 'full' | 'large' | 'focused'; onCycleWebFullscreenSize: () => void; containerRef: React.RefObject; isRotated?: boolean; } export function DesktopMoreMenu({ showMoreMenu, isPremium = false, isProxied = false, onToggleMoreMenu, onMouseEnter, onMouseLeave, onCopyLink, webFullscreenSize, onCycleWebFullscreenSize, containerRef, isRotated = false }: DesktopMoreMenuProps) { const { autoNextEpisode, autoSkipIntro, skipIntroSeconds, autoSkipOutro, skipOutroSeconds, showModeIndicator, adFilter, setAutoNextEpisode, setAutoSkipIntro, setSkipIntroSeconds, setAutoSkipOutro, setSkipOutroSeconds, setShowModeIndicator, setAdFilter, adFilterMode, setAdFilterMode, fullscreenType, setFullscreenType, danmakuEnabled, setDanmakuEnabled, danmakuApiUrl, danmakuOpacity, setDanmakuOpacity, danmakuFontSize, setDanmakuFontSize, danmakuDisplayArea, setDanmakuDisplayArea, } = usePlayerSettings(isPremium); 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 WEB_FULLSCREEN_SIZE_LABELS: Record<'full' | 'large' | 'focused', string> = { full: '铺满窗口', large: '大窗模式', focused: '聚焦影院', }; 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 && !isFullscreen) { // Normal Mode: Non-rotated, non-fullscreen // Use Viewport Coordinates but position relative to 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 if (isFullscreen && !isRotated) { // Fullscreen Mode (not rotated): Use container-relative coordinates // Portal goes to containerRef to stay visible within fullscreen element 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 containerWidth = containerRef.current.offsetWidth; const containerHeight = containerRef.current.offsetHeight; const spaceBelow = containerHeight - (top + buttonHeight) - 10; const spaceAbove = 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, containerHeight * 0.7); const isLeftHalf = left < containerWidth / 2; const align = isLeftHalf ? 'left' : 'right'; setMenuPosition({ top: openUpward ? top - 10 : top + buttonHeight + 10, left: isLeftHalf ? left : left + buttonWidth, 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, isFullscreen]); // 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]) => ( ))}
)}
{/* Divider */}
{/* Danmaku Toggle */}
弹幕 {!danmakuApiUrl && ( (未配置) )}
{/* Danmaku Sub-Settings (shown when enabled and configured) */} {danmakuEnabled && danmakuApiUrl && (
{/* Opacity Slider */}
透明度 {Math.round(danmakuOpacity * 100)}%
setDanmakuOpacity(parseInt(e.target.value) / 100)} className={`w-full accent-[var(--accent-color)] ${isRotated ? 'h-1' : 'h-1.5'}`} onClick={(e) => e.stopPropagation()} />
{/* Font Size Buttons */}
字号
{[14, 18, 20, 24, 28].map((size) => ( ))}
{/* Display Area Buttons */}
显示区域
{([ { value: 0.25, label: '1/4屏' }, { value: 0.5, label: '半屏' }, { value: 0.75, label: '3/4屏' }, { value: 1.0, label: '全屏' }, ] as const).map(({ value, 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 || isFullscreen) && containerRef.current) ? containerRef.current : document.body)}
); }