diff --git a/components/player/CustomVideoPlayer.tsx b/components/player/CustomVideoPlayer.tsx index cb7e0f1..f53ac03 100644 --- a/components/player/CustomVideoPlayer.tsx +++ b/components/player/CustomVideoPlayer.tsx @@ -1,8 +1,7 @@ 'use client'; -import { useIsMobile } from '@/lib/hooks/useMobilePlayer'; import { DesktopVideoPlayer } from './DesktopVideoPlayer'; -import { MobileVideoPlayer } from './MobileVideoPlayer'; + interface CustomVideoPlayerProps { src: string; @@ -23,9 +22,5 @@ interface CustomVideoPlayerProps { * - Desktop: Full-featured player with hover interactions */ export function CustomVideoPlayer(props: CustomVideoPlayerProps) { - const isMobile = useIsMobile(); - - return isMobile - ? - : ; + return ; } diff --git a/components/player/DesktopVideoPlayer.tsx b/components/player/DesktopVideoPlayer.tsx index ff3c623..367284d 100644 --- a/components/player/DesktopVideoPlayer.tsx +++ b/components/player/DesktopVideoPlayer.tsx @@ -109,7 +109,33 @@ export function DesktopVideoPlayer({ state.setShowMoreMenu(!state.showMoreMenu)} + onMoreMenuMouseEnter={() => { + if (refs.moreMenuTimeoutRef.current) { + clearTimeout(refs.moreMenuTimeoutRef.current); + } + }} + onMoreMenuMouseLeave={() => { + refs.moreMenuTimeoutRef.current = setTimeout(() => { + state.setShowMoreMenu(false); + }, 300); + }} + onCopyLink={logic.handleCopyLink} + // Speed Menu Props + playbackRate={state.playbackRate} + showSpeedMenu={state.showSpeedMenu} + speeds={[0.5, 0.75, 1, 1.25, 1.5, 2]} + onToggleSpeedMenu={() => state.setShowSpeedMenu(!state.showSpeedMenu)} + onSpeedChange={logic.changePlaybackSpeed} + onSpeedMenuMouseEnter={logic.clearSpeedMenuTimeout} + onSpeedMenuMouseLeave={logic.startSpeedMenuTimeout} /> void; - onTimeUpdate?: (currentTime: number, duration: number) => void; - initialTime?: number; - shouldAutoPlay?: boolean; - // Episode navigation props for auto-skip/auto-next - totalEpisodes?: number; - currentEpisodeIndex?: number; - onNextEpisode?: () => void; -} - -export function MobileVideoPlayer({ - src, - poster, - onError, - onTimeUpdate, - initialTime = 0, - shouldAutoPlay = false, - totalEpisodes = 1, - currentEpisodeIndex = 0, - onNextEpisode, -}: MobileVideoPlayerProps) { - const { refs, state } = useMobilePlayerState(); - - const { - videoRef, - containerRef, - controlsTimeoutRef - } = refs; - - // Initialize HLS Player (same as Desktop - fixes Android Chrome playback) - useHlsPlayer({ - videoRef, - src, - autoPlay: shouldAutoPlay, - onError, - }); - - const { - isPlaying, - isFullscreen, - showControls, - isLoading, - showSkipIndicator, - skipAmount, - skipSide, - toastMessage, - showToast, - currentTime, - duration, - setShowControls, - setIsLoading - } = state; - - const logic = useMobilePlayerLogic({ - src, - initialTime, - shouldAutoPlay, - onError, - onTimeUpdate, - refs, - state - }); - - // Auto-skip intro/outro and auto-next episode - useAutoSkip({ - videoRef, - currentTime, - duration, - isPlaying, - totalEpisodes, - currentEpisodeIndex, - onNextEpisode, - }); - - const { - skipVideo, - togglePlay, - handlePlay, - handlePause, - handleTimeUpdateEvent, - handleLoadedMetadata, - handleVideoError, - } = logic; - - // Screen orientation management - useScreenOrientation(isFullscreen); - - // Double tap handler - const { handleTap } = useMobileGestures({ - skipVideo, - showSkipIndicator, - showControls, - setShowControls, - controlsTimeoutRef, - isPlaying, - togglePlay, - }); - - return ( -
- {/* Video Element - src is set by useHlsPlayer hook */} -
- ); -} diff --git a/components/player/desktop/DesktopControls.tsx b/components/player/desktop/DesktopControls.tsx index 620a5b2..fbc853e 100644 --- a/components/player/desktop/DesktopControls.tsx +++ b/components/player/desktop/DesktopControls.tsx @@ -11,9 +11,8 @@ interface DesktopControlsProps { volume: number; isMuted: boolean; isFullscreen: boolean; - playbackRate: number; - showSpeedMenu: boolean; - showMoreMenu: boolean; + + showVolumeBar: boolean; isPiPSupported: boolean; isAirPlaySupported: boolean; @@ -22,8 +21,6 @@ interface DesktopControlsProps { progressBarRef: React.RefObject; volumeBarRef: React.RefObject; onTogglePlay: () => void; - onSkipForward: () => void; - onSkipBackward: () => void; onToggleMute: () => void; onVolumeChange: (e: React.MouseEvent) => void; onVolumeMouseDown: (e: React.MouseEvent) => void; @@ -31,18 +28,9 @@ interface DesktopControlsProps { onTogglePictureInPicture: () => void; onShowAirPlayMenu: () => void; onShowCastMenu: () => void; - onToggleSpeedMenu: () => void; - onToggleMoreMenu: () => void; - onSpeedChange: (speed: number) => void; - onCopyLink: (type?: 'original' | 'proxy') => void; onProgressClick: (e: React.MouseEvent) => void; onProgressMouseDown: (e: React.MouseEvent) => void; - onSpeedMenuMouseEnter: () => void; - onSpeedMenuMouseLeave: () => void; - onMoreMenuMouseEnter: () => void; - onMoreMenuMouseLeave: () => void; formatTime: (seconds: number) => string; - speeds: number[]; } export function DesktopControls(props: DesktopControlsProps) { diff --git a/components/player/desktop/DesktopControlsWrapper.tsx b/components/player/desktop/DesktopControlsWrapper.tsx index 9f64098..85facc7 100644 --- a/components/player/desktop/DesktopControlsWrapper.tsx +++ b/components/player/desktop/DesktopControlsWrapper.tsx @@ -19,21 +19,14 @@ export function DesktopControlsWrapper({ src, state, logic, refs }: DesktopContr isMuted, isFullscreen, showControls, - playbackRate, - showSpeedMenu, - showMoreMenu, showVolumeBar, isPiPSupported, isAirPlaySupported, isCastAvailable, - setShowSpeedMenu, - setShowMoreMenu, } = state; const { togglePlay, - skipForward, - skipBackward, toggleMute, handleVolumeChange, handleVolumeMouseDown, @@ -41,22 +34,16 @@ export function DesktopControlsWrapper({ src, state, logic, refs }: DesktopContr togglePictureInPicture, showAirPlayMenu, showCastMenu, - changePlaybackSpeed, - handleCopyLink, handleProgressClick, handleProgressMouseDown, - startSpeedMenuTimeout, - clearSpeedMenuTimeout, formatTime, } = logic; const { progressBarRef, volumeBarRef, - moreMenuTimeoutRef, } = refs; - const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2]; const isProxied = src.includes('/api/proxy'); return ( @@ -68,9 +55,6 @@ export function DesktopControlsWrapper({ src, state, logic, refs }: DesktopContr volume={volume} isMuted={isMuted} isFullscreen={isFullscreen} - playbackRate={playbackRate} - showSpeedMenu={showSpeedMenu} - showMoreMenu={showMoreMenu} showVolumeBar={showVolumeBar} isPiPSupported={isPiPSupported} isAirPlaySupported={isAirPlaySupported} @@ -79,8 +63,6 @@ export function DesktopControlsWrapper({ src, state, logic, refs }: DesktopContr progressBarRef={progressBarRef} volumeBarRef={volumeBarRef} onTogglePlay={togglePlay} - onSkipForward={skipForward} - onSkipBackward={skipBackward} onToggleMute={toggleMute} onVolumeChange={handleVolumeChange} onVolumeMouseDown={handleVolumeMouseDown} @@ -88,26 +70,9 @@ export function DesktopControlsWrapper({ src, state, logic, refs }: DesktopContr onTogglePictureInPicture={togglePictureInPicture} onShowAirPlayMenu={showAirPlayMenu} onShowCastMenu={showCastMenu} - onToggleSpeedMenu={() => setShowSpeedMenu(!showSpeedMenu)} - onToggleMoreMenu={() => setShowMoreMenu(!showMoreMenu)} - onSpeedChange={changePlaybackSpeed} - onCopyLink={handleCopyLink} onProgressClick={handleProgressClick} onProgressMouseDown={handleProgressMouseDown} - onSpeedMenuMouseEnter={clearSpeedMenuTimeout} - onSpeedMenuMouseLeave={startSpeedMenuTimeout} - onMoreMenuMouseEnter={() => { - if (moreMenuTimeoutRef.current) { - clearTimeout(moreMenuTimeoutRef.current); - } - }} - onMoreMenuMouseLeave={() => { - moreMenuTimeoutRef.current = setTimeout(() => { - setShowMoreMenu(false); - }, 300); - }} formatTime={formatTime} - speeds={speeds} /> ); } diff --git a/components/player/desktop/DesktopLeftControls.tsx b/components/player/desktop/DesktopLeftControls.tsx index 674a7ad..f545e4c 100644 --- a/components/player/desktop/DesktopLeftControls.tsx +++ b/components/player/desktop/DesktopLeftControls.tsx @@ -11,8 +11,6 @@ interface DesktopLeftControlsProps { showVolumeBar: boolean; volumeBarRef: React.RefObject; onTogglePlay: () => void; - onSkipForward: () => void; - onSkipBackward: () => void; onToggleMute: () => void; onVolumeChange: (e: React.MouseEvent) => void; onVolumeMouseDown: (e: React.MouseEvent) => void; @@ -28,8 +26,6 @@ export function DesktopLeftControls({ showVolumeBar, volumeBarRef, onTogglePlay, - onSkipForward, - onSkipBackward, onToggleMute, onVolumeChange, onVolumeMouseDown, @@ -46,26 +42,6 @@ export function DesktopLeftControls({ {isPlaying ? : } - {/* Skip Backward 10s */} - - - {/* Skip Forward 10s */} - - {/* Volume */} (null); + const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0 }); + + React.useEffect(() => { + if (showMoreMenu && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setMenuPosition({ + top: rect.bottom + 10, // 10px spacing + left: rect.left + }); + } + }, [showMoreMenu]); + + const handleToggle = () => { + if (!showMoreMenu && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setMenuPosition({ + top: rect.bottom + 10, // 10px spacing + left: rect.left + }); + } + onToggleMoreMenu(); + }; + + const MenuContent = ( +
e.stopPropagation()} + > + {/* Copy Link Options */} + {isProxied ? ( + <> + + + + ) : ( + + )} + + {/* Divider */} +
+ + {/* Show Mode Indicator Switch */} +
+
+ + 显示模式指示器 +
+ +
+ + {/* Auto Next Episode Switch */} +
+
+ + 自动下一集 +
+ +
+ + {/* Skip Intro Switch */} +
+
+
+ + 跳过片头 +
+ +
+ {/* Expandable Input */} + {autoSkipIntro && ( +
+ 时长: + setSkipIntroSeconds(parseInt(e.target.value) || 0)} + className="w-16 px-2 py-1 text-sm 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" + onClick={(e) => e.stopPropagation()} + /> + +
+ )} +
+ + {/* Skip Outro Switch */} +
+
+
+ + 跳过片尾 +
+ +
+ {/* Expandable Input */} + {autoSkipOutro && ( +
+ 剩余: + setSkipOutroSeconds(parseInt(e.target.value) || 0)} + className="w-16 px-2 py-1 text-sm 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" + onClick={(e) => e.stopPropagation()} + /> + +
+ )} +
+
+ ); + return (
- {/* More Menu Dropdown */} - {showMoreMenu && ( -
e.stopPropagation()} - > - {/* Copy Link Options */} - {isProxied ? ( - <> - - - - ) : ( - - )} - - {/* Divider */} -
- - {/* Show Mode Indicator Switch */} -
-
- - 显示模式指示器 -
- -
- - {/* Auto Next Episode Switch */} -
-
- - 自动下一集 -
- -
- - {/* Skip Intro Switch */} -
-
-
- - 跳过片头 -
- -
- {/* Expandable Input */} - {autoSkipIntro && ( -
- 时长: - setSkipIntroSeconds(parseInt(e.target.value) || 0)} - className="w-16 px-2 py-1 text-sm 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" - onClick={(e) => e.stopPropagation()} - /> - -
- )} -
- - {/* Skip Outro Switch */} -
-
-
- - 跳过片尾 -
- -
- {/* Expandable Input */} - {autoSkipOutro && ( -
- 剩余: - setSkipOutroSeconds(parseInt(e.target.value) || 0)} - className="w-16 px-2 py-1 text-sm 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" - onClick={(e) => e.stopPropagation()} - /> - -
- )} -
-
- )} + {/* More Menu Dropdown (Portal) */} + {showMoreMenu && typeof document !== 'undefined' && createPortal(MenuContent, document.body)}
); } diff --git a/components/player/desktop/DesktopOverlay.tsx b/components/player/desktop/DesktopOverlay.tsx index 9bddd86..b127f99 100644 --- a/components/player/desktop/DesktopOverlay.tsx +++ b/components/player/desktop/DesktopOverlay.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { Icons } from '@/components/ui/Icon'; +import { DesktopMoreMenu } from './DesktopMoreMenu'; +import { DesktopSpeedMenu } from './DesktopSpeedMenu'; + interface DesktopOverlayProps { isLoading: boolean; isPlaying: boolean; @@ -12,7 +15,24 @@ interface DesktopOverlayProps { isSkipBackwardAnimatingOut: boolean; showToast: boolean; toastMessage: string | null; + showControls: boolean; onTogglePlay: () => void; + onSkipForward: () => void; + onSkipBackward: () => void; + showMoreMenu: boolean; + isProxied: boolean; + onToggleMoreMenu: () => void; + onMoreMenuMouseEnter: () => void; + onMoreMenuMouseLeave: () => void; + onCopyLink: (type?: 'original' | 'proxy') => void; + // Speed Menu Props + playbackRate: number; + showSpeedMenu: boolean; + speeds: number[]; + onToggleSpeedMenu: () => void; + onSpeedChange: (speed: number) => void; + onSpeedMenuMouseEnter: () => void; + onSpeedMenuMouseLeave: () => void; } export function DesktopOverlay({ @@ -26,10 +46,54 @@ export function DesktopOverlay({ isSkipBackwardAnimatingOut, showToast, toastMessage, - onTogglePlay + onTogglePlay, + onSkipForward, + onSkipBackward, + showControls, + showMoreMenu, + isProxied, + onToggleMoreMenu, + onMoreMenuMouseEnter, + onMoreMenuMouseLeave, + onCopyLink, + playbackRate, + showSpeedMenu, + speeds, + onToggleSpeedMenu, + onSpeedChange, + onSpeedMenuMouseEnter, + onSpeedMenuMouseLeave }: DesktopOverlayProps) { + // Show navigation buttons when controls are visible or when paused (controls usually show when paused anyway) + const showNavButtons = showControls || !isPlaying; + return ( <> + {/* More Menu (Top Left) */} +
+ +
+ + {/* Speed Menu (Top Right) */} +
+ +
+ {/* Loading Spinner - Glass Effect */} {isLoading && (
@@ -37,29 +101,65 @@ export function DesktopOverlay({
)} - {/* Skip Forward Indicator */} - {showSkipForwardIndicator && ( -
-
+
- +{skipForwardAmount} + -{skipBackwardAmount}s
)} - {/* Skip Backward Indicator */} - {showSkipBackwardIndicator && ( -
-
+
- -{skipBackwardAmount} + +{skipForwardAmount}s
)} + {/* Previous Button (Method: Skip Backward) */} +
+ +
+ + {/* Next Button (Method: Skip Forward) */} +
+ +
+ {/* Center Play Button (when paused) */} {!isPlaying && !isLoading && ( -
+
- )} + { + isPiPSupported && ( + + ) + } {/* AirPlay */} - {isAirPlaySupported && ( - - )} + { + isAirPlaySupported && ( + + ) + } {/* Google Cast */} - {isCastAvailable && ( - - )} - - {/* More Menu */} - + { + isCastAvailable && ( + + ) + } {/* Fullscreen */} -
+
); } diff --git a/components/player/desktop/DesktopSpeedMenu.tsx b/components/player/desktop/DesktopSpeedMenu.tsx index 934e7bd..c2d4e50 100644 --- a/components/player/desktop/DesktopSpeedMenu.tsx +++ b/components/player/desktop/DesktopSpeedMenu.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { createPortal } from 'react-dom'; interface DesktopSpeedMenuProps { showSpeedMenu: boolean; @@ -19,39 +20,72 @@ export function DesktopSpeedMenu({ onMouseEnter, onMouseLeave }: DesktopSpeedMenuProps) { + const buttonRef = React.useRef(null); + const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0 }); + + React.useEffect(() => { + if (showSpeedMenu && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setMenuPosition({ + top: rect.bottom + 10, + left: rect.right // Align with right edge + }); + } + }, [showSpeedMenu]); + + const handleToggle = () => { + if (!showSpeedMenu && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setMenuPosition({ + top: rect.bottom + 10, + left: rect.right // Align with right edge + }); + } + onToggleSpeedMenu(); + }; + + + const MenuContent = ( +
+ {speeds.map((speed) => ( + + ))} +
+ ); + return (
- {/* Speed Menu */} - {showSpeedMenu && ( -
- {speeds.map((speed) => ( - - ))} -
- )} + {/* Speed Menu (Portal) */} + {showSpeedMenu && typeof document !== 'undefined' && createPortal(MenuContent, document.body)}
); } diff --git a/components/player/hooks/mobile/mobile-player-params.ts b/components/player/hooks/mobile/mobile-player-params.ts deleted file mode 100644 index 454460d..0000000 --- a/components/player/hooks/mobile/mobile-player-params.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Parameter builder utilities for mobile player hooks - */ - -export function buildPlaybackParams(props: any) { - const { - videoRef, - isPlaying, - setIsPlaying, - setIsLoading, - initialTime, - shouldAutoPlay, - setDuration, - setCurrentTime, - setPlaybackRate, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - onTimeUpdate, - onError, - isDraggingProgressRef, - isTogglingRef, - } = props; - - return { - videoRef, - isPlaying, - setIsPlaying, - setIsLoading, - initialTime, - shouldAutoPlay, - setDuration, - setCurrentTime, - setPlaybackRate, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - onTimeUpdate, - onError, - isDraggingProgressRef, - isTogglingRef, - }; -} - -export function buildProgressParams(props: any) { - const { videoRef, progressBarRef, duration, setCurrentTime, isDraggingProgressRef } = props; - return { videoRef, progressBarRef, duration, setCurrentTime, isDraggingProgressRef }; -} - -export function buildSkipParams(props: any) { - const { - videoRef, - duration, - setCurrentTime, - skipAmount, - skipSide, - setSkipAmount, - setSkipSide, - setShowSkipIndicator, - skipTimeoutRef, - } = props; - - return { - videoRef, - duration, - setCurrentTime, - skipAmount, - skipSide, - setSkipAmount, - setSkipSide, - setShowSkipIndicator, - skipTimeoutRef, - }; -} - -export function buildFullscreenParams(props: any) { - const { containerRef, videoRef, isFullscreen, setIsFullscreen, isPiPSupported, setIsPiPSupported } = props; - return { containerRef, videoRef, isFullscreen, setIsFullscreen, isPiPSupported, setIsPiPSupported }; -} - -export function buildUtilitiesParams(props: any) { - const { - src, - volume, - isMuted, - videoRef, - setVolume, - setIsMuted, - setViewportWidth, - setToastMessage, - setShowToast, - toastTimeoutRef, - } = props; - - return { - src, - volume, - isMuted, - videoRef, - setVolume, - setIsMuted, - setViewportWidth, - setToastMessage, - setShowToast, - toastTimeoutRef, - }; -} - -export function buildMenuParams(props: any) { - const { - videoRef, - isPlaying, - showMoreMenu, - showVolumeMenu, - showSpeedMenu, - wasPlayingBeforeMenu, - setShowControls, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - setWasPlayingBeforeMenu, - controlsTimeoutRef, - menuIdleTimeoutRef, - } = props; - - return { - videoRef, - isPlaying, - showMoreMenu, - showVolumeMenu, - showSpeedMenu, - wasPlayingBeforeMenu, - setShowControls, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - setWasPlayingBeforeMenu, - controlsTimeoutRef, - menuIdleTimeoutRef, - }; -} diff --git a/components/player/hooks/mobile/useMobileFullscreenControls.ts b/components/player/hooks/mobile/useMobileFullscreenControls.ts deleted file mode 100644 index b035b65..0000000 --- a/components/player/hooks/mobile/useMobileFullscreenControls.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { useIsIOS } from '@/lib/hooks/useMobilePlayer'; - -interface UseMobileFullscreenProps { - containerRef: React.RefObject; - videoRef: React.RefObject; - isFullscreen: boolean; - setIsFullscreen: (fullscreen: boolean) => void; - isPiPSupported: boolean; - setIsPiPSupported: (supported: boolean) => void; -} - -export function useMobileFullscreenControls({ - containerRef, - videoRef, - isFullscreen, - setIsFullscreen, - isPiPSupported, - setIsPiPSupported -}: UseMobileFullscreenProps) { - const isIOS = useIsIOS(); - - useEffect(() => { - if (typeof document !== 'undefined') { - setIsPiPSupported('pictureInPictureEnabled' in document); - } - }, [setIsPiPSupported]); - - const toggleFullscreen = useCallback(() => { - if (!containerRef.current) return; - - if (!isFullscreen) { - if (isIOS && videoRef.current && (videoRef.current as any).webkitEnterFullscreen) { - (videoRef.current as any).webkitEnterFullscreen(); - return; - } - - if (containerRef.current.requestFullscreen) { - containerRef.current.requestFullscreen().catch((err: Error) => console.warn('Fullscreen request failed:', err)); - } else if ((containerRef.current as any).webkitRequestFullscreen) { - (containerRef.current as any).webkitRequestFullscreen(); - } else if ((containerRef.current as any).webkitRequestFullScreen) { - (containerRef.current as any).webkitRequestFullScreen(); - } - } else { - if (document.exitFullscreen) { - document.exitFullscreen().catch((err: Error) => console.warn('Exit fullscreen failed:', err)); - } else if ((document as any).webkitExitFullscreen) { - (document as any).webkitExitFullscreen(); - } else if ((document as any).webkitCancelFullScreen) { - (document as any).webkitCancelFullScreen(); - } - } - }, [containerRef, isFullscreen, isIOS, videoRef]); - - useEffect(() => { - const handleFullscreenChange = () => { - const isInFullscreen = !!( - document.fullscreenElement || - (document as any).webkitFullscreenElement || - (document as any).webkitCurrentFullScreenElement - ); - setIsFullscreen(isInFullscreen); - }; - - document.addEventListener('fullscreenchange', handleFullscreenChange); - document.addEventListener('webkitfullscreenchange', handleFullscreenChange); - document.addEventListener('mozfullscreenchange', handleFullscreenChange); - document.addEventListener('MSFullscreenChange', handleFullscreenChange); - - return () => { - document.removeEventListener('fullscreenchange', handleFullscreenChange); - document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); - document.removeEventListener('mozfullscreenchange', handleFullscreenChange); - document.removeEventListener('MSFullscreenChange', handleFullscreenChange); - }; - }, [setIsFullscreen]); - - const togglePictureInPicture = useCallback(async () => { - if (!videoRef.current || !isPiPSupported) return; - - try { - if (document.pictureInPictureElement) { - await document.exitPictureInPicture(); - } else { - await videoRef.current.requestPictureInPicture(); - } - } catch (error) { - console.error('Failed to toggle Picture-in-Picture:', error); - } - }, [videoRef, isPiPSupported]); - - return { - toggleFullscreen, - togglePictureInPicture - }; -} diff --git a/components/player/hooks/mobile/useMobileMenuControls.ts b/components/player/hooks/mobile/useMobileMenuControls.ts deleted file mode 100644 index 835a163..0000000 --- a/components/player/hooks/mobile/useMobileMenuControls.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { useEffect } from 'react'; - -interface UseMobileMenuControlsProps { - videoRef: React.RefObject; - isPlaying: boolean; - showMoreMenu: boolean; - showVolumeMenu: boolean; - showSpeedMenu: boolean; - wasPlayingBeforeMenu: boolean; - setShowControls: (show: boolean) => void; - setShowMoreMenu: (show: boolean) => void; - setShowVolumeMenu: (show: boolean) => void; - setShowSpeedMenu: (show: boolean) => void; - setWasPlayingBeforeMenu: (was: boolean) => void; - controlsTimeoutRef: React.MutableRefObject; - menuIdleTimeoutRef: React.MutableRefObject; -} - -export function useMobileMenuControls({ - videoRef, - isPlaying, - showMoreMenu, - showVolumeMenu, - showSpeedMenu, - wasPlayingBeforeMenu, - setShowControls, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - setWasPlayingBeforeMenu, - controlsTimeoutRef, - menuIdleTimeoutRef -}: UseMobileMenuControlsProps) { - useEffect(() => { - if (!isPlaying) { - setShowControls(true); - if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); - return; - } - - const hideControls = () => { - if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); - controlsTimeoutRef.current = setTimeout(() => { - if (isPlaying) { - setShowControls(false); - setShowSpeedMenu(false); - setShowVolumeMenu(false); - setShowMoreMenu(false); - } - }, 3000); - }; - - hideControls(); - return () => { - if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); - }; - }, [isPlaying, setShowControls, setShowSpeedMenu, setShowVolumeMenu, setShowMoreMenu, controlsTimeoutRef]); - - useEffect(() => { - if (showMoreMenu) { - if (videoRef.current && isPlaying) { - setWasPlayingBeforeMenu(true); - videoRef.current.pause(); - } - if (menuIdleTimeoutRef.current) clearTimeout(menuIdleTimeoutRef.current); - menuIdleTimeoutRef.current = setTimeout(() => { - setShowMoreMenu(false); - if (wasPlayingBeforeMenu && videoRef.current) { - videoRef.current.play().catch((err: Error) => console.warn('Resume play error:', err)); - setWasPlayingBeforeMenu(false); - } - }, 2000); - } - return () => { - if (menuIdleTimeoutRef.current) clearTimeout(menuIdleTimeoutRef.current); - }; - }, [showMoreMenu, isPlaying, wasPlayingBeforeMenu, videoRef, menuIdleTimeoutRef, setShowMoreMenu, setWasPlayingBeforeMenu]); - - useEffect(() => { - if (showVolumeMenu || showSpeedMenu) { - if (videoRef.current && isPlaying) { - setWasPlayingBeforeMenu(true); - videoRef.current.pause(); - } - } - }, [showVolumeMenu, showSpeedMenu, isPlaying, videoRef, setWasPlayingBeforeMenu]); - - useEffect(() => { - const handleClickOutside = (e: any) => { - const target = e.target as HTMLElement; - const isMenuClick = target.closest('.menu-container') || target.closest('[aria-label="更多"]'); - - if (!isMenuClick && (showMoreMenu || showVolumeMenu || showSpeedMenu)) { - setShowMoreMenu(false); - setShowVolumeMenu(false); - setShowSpeedMenu(false); - - if (wasPlayingBeforeMenu && videoRef.current) { - videoRef.current.play().catch((err: Error) => console.warn('Resume play error:', err)); - setWasPlayingBeforeMenu(false); - } - } - }; - - if (showMoreMenu || showVolumeMenu || showSpeedMenu) { - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('touchstart', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('touchstart', handleClickOutside); - }; - }, [showMoreMenu, showVolumeMenu, showSpeedMenu, wasPlayingBeforeMenu, videoRef, setShowMoreMenu, setShowVolumeMenu, setShowSpeedMenu, setWasPlayingBeforeMenu]); -} diff --git a/components/player/hooks/mobile/useMobilePlaybackControls.ts b/components/player/hooks/mobile/useMobilePlaybackControls.ts deleted file mode 100644 index 35305a1..0000000 --- a/components/player/hooks/mobile/useMobilePlaybackControls.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react'; -import { formatTime } from '@/lib/utils/format-utils'; -import { usePlaybackPolling } from '../usePlaybackPolling'; -import { useMobileTogglePlay } from './useMobileTogglePlay'; - -interface UseMobilePlaybackProps { - videoRef: React.RefObject; - isPlaying: boolean; - setIsPlaying: (playing: boolean) => void; - setIsLoading: (loading: boolean) => void; - initialTime: number; - shouldAutoPlay: boolean; - setDuration: (duration: number) => void; - setCurrentTime: (time: number) => void; - setPlaybackRate: (rate: number) => void; - setShowMoreMenu: (show: boolean) => void; - setShowVolumeMenu: (show: boolean) => void; - setShowSpeedMenu: (show: boolean) => void; - onTimeUpdate?: (currentTime: number, duration: number) => void; - onError?: (error: string) => void; - isDraggingProgressRef: React.MutableRefObject; - isTogglingRef: React.MutableRefObject; -} - -export function useMobilePlaybackControls({ - videoRef, - isPlaying, - setIsPlaying, - setIsLoading, - initialTime, - shouldAutoPlay, - setDuration, - setCurrentTime, - setPlaybackRate, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - onTimeUpdate, - onError, - isDraggingProgressRef, - isTogglingRef -}: UseMobilePlaybackProps) { - const togglePlay = useMobileTogglePlay({ - videoRef, - isPlaying, - isTogglingRef, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu - }); - - const handlePlay = useCallback(() => setIsPlaying(true), [setIsPlaying]); - const handlePause = useCallback(() => setIsPlaying(false), [setIsPlaying]); - - const handleTimeUpdateEvent = useCallback(() => { - if (!videoRef.current || isDraggingProgressRef.current) return; - const current = videoRef.current.currentTime; - const total = videoRef.current.duration; - setCurrentTime(current); - setDuration(total); - if (onTimeUpdate) { - onTimeUpdate(current, total); - } - }, [videoRef, isDraggingProgressRef, setCurrentTime, setDuration, onTimeUpdate]); - - const handleLoadedMetadata = useCallback(() => { - if (!videoRef.current) return; - setDuration(videoRef.current.duration); - setIsLoading(false); - - // Fix for stuck at 00:00:00: - // Only seek if we are at the very start (to avoid overwriting a previous seek) - if (videoRef.current.currentTime < 0.5) { - // If initialTime is 0, we seek to a tiny offset to help the browser/HLS buffer start. - const startPosition = initialTime > 0 ? initialTime : 0.1; - videoRef.current.currentTime = startPosition; - } - - videoRef.current.play().catch((err: Error) => { - console.warn('Autoplay was prevented:', err); - }); - }, [videoRef, setDuration, setIsLoading, initialTime]); - - const hasInitialSeekHappened = useRef(false); - - // Handle late initialization of initialTime (e.g. from async storage hydration) - useEffect(() => { - if (initialTime > 0 && videoRef.current && !hasInitialSeekHappened.current) { - // Only seek if we haven't progressed far (e.g. still near start) - // AND if the target time is significantly different from current time (> 0.5s) - // This prevents jumping if the user has already started watching and initialTime updates - if (videoRef.current.currentTime < 2 && Math.abs(videoRef.current.currentTime - initialTime) > 0.5) { - videoRef.current.currentTime = initialTime; - hasInitialSeekHappened.current = true; - } else if (videoRef.current.currentTime >= 2) { - // If user already watched past 2s, assume they don't want to be reset - hasInitialSeekHappened.current = true; - } - } - }, [initialTime, videoRef]); - - // Force autoplay when shouldAutoPlay is true (for proxy retry) - useEffect(() => { - if (shouldAutoPlay && videoRef.current) { - const playPromise = videoRef.current.play(); - if (playPromise !== undefined) { - playPromise.catch((err: Error) => { - console.warn('Force autoplay was prevented:', err); - }); - } - } - }, [shouldAutoPlay, videoRef]); - - const handleVideoError = useCallback(() => { - setIsLoading(false); - if (onError) { - onError('Video failed to load'); - } - }, [setIsLoading, onError]); - - const changePlaybackSpeed = useCallback((speed: number) => { - if (!videoRef.current) return; - videoRef.current.playbackRate = speed; - setPlaybackRate(speed); - setShowSpeedMenu(false); - }, [videoRef, setPlaybackRate, setShowSpeedMenu]); - - // Polling fallback for AirPlay and throttled events - usePlaybackPolling({ - isPlaying, - videoRef, - isDraggingProgressRef, - setCurrentTime, - setDuration, - setIsPlaying - }); - - return { - togglePlay, - handlePlay, - handlePause, - handleTimeUpdateEvent, - handleLoadedMetadata, - handleVideoError, - changePlaybackSpeed, - formatTime - }; -} diff --git a/components/player/hooks/mobile/useMobileProgressControls.ts b/components/player/hooks/mobile/useMobileProgressControls.ts deleted file mode 100644 index 88e0fd8..0000000 --- a/components/player/hooks/mobile/useMobileProgressControls.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useCallback, useEffect } from 'react'; - -interface UseMobileProgressControlsProps { - videoRef: React.RefObject; - progressBarRef: React.RefObject; - duration: number; - setCurrentTime: (time: number) => void; - isDraggingProgressRef: React.MutableRefObject; -} - -export function useMobileProgressControls({ - videoRef, - progressBarRef, - duration, - setCurrentTime, - isDraggingProgressRef -}: UseMobileProgressControlsProps) { - const updateProgressFromEvent = useCallback((e: any) => { - if (!videoRef.current || !progressBarRef.current) return; - - const rect = progressBarRef.current.getBoundingClientRect(); - let clientX: number; - - if ('touches' in e) { - const touch = e.touches[0] || e.changedTouches?.[0]; - if (!touch) return; - clientX = touch.clientX; - } else { - clientX = e.clientX; - } - - const pos = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); - return pos * duration; - }, [videoRef, progressBarRef, duration]); - - const handleProgressTouchStart = useCallback((e: any) => { - isDraggingProgressRef.current = true; - const newTime = updateProgressFromEvent(e); - if (newTime !== undefined) { - setCurrentTime(newTime); - } - }, [isDraggingProgressRef, updateProgressFromEvent, setCurrentTime]); - - const handleProgressTouchMove = useCallback((e: any) => { - if (!isDraggingProgressRef.current) return; - e.preventDefault(); - const newTime = updateProgressFromEvent(e); - if (newTime !== undefined) { - setCurrentTime(newTime); - } - }, [isDraggingProgressRef, updateProgressFromEvent, setCurrentTime]); - - const handleProgressTouchEnd = useCallback((e: any) => { - if (!isDraggingProgressRef.current) return; - isDraggingProgressRef.current = false; - const newTime = updateProgressFromEvent(e); - if (newTime !== undefined && videoRef.current) { - videoRef.current.currentTime = newTime; - setCurrentTime(newTime); - } - }, [isDraggingProgressRef, updateProgressFromEvent, videoRef, setCurrentTime]); - - const handleProgressClick = useCallback((e: any) => { - const newTime = updateProgressFromEvent(e); - if (newTime !== undefined && videoRef.current) { - videoRef.current.currentTime = newTime; - setCurrentTime(newTime); - } - }, [updateProgressFromEvent, videoRef, setCurrentTime]); - - return { - handleProgressTouchStart, - handleProgressTouchMove, - handleProgressTouchEnd, - handleProgressClick - }; -} diff --git a/components/player/hooks/mobile/useMobileSkipControls.ts b/components/player/hooks/mobile/useMobileSkipControls.ts deleted file mode 100644 index eac1e10..0000000 --- a/components/player/hooks/mobile/useMobileSkipControls.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useCallback } from 'react'; - -interface UseMobileSkipControlsProps { - videoRef: React.RefObject; - duration: number; - setCurrentTime: (time: number) => void; - skipAmount: number; - skipSide: 'left' | 'right' | null; - setSkipAmount: (amount: number) => void; - setSkipSide: (side: 'left' | 'right' | null) => void; - setShowSkipIndicator: (show: boolean) => void; - skipTimeoutRef: React.MutableRefObject; -} - -export function useMobileSkipControls({ - videoRef, - duration, - setCurrentTime, - skipAmount, - skipSide, - setSkipAmount, - setSkipSide, - setShowSkipIndicator, - skipTimeoutRef -}: UseMobileSkipControlsProps) { - const skipVideo = useCallback((seconds: number, side: 'left' | 'right') => { - if (!videoRef.current) return; - - if (skipTimeoutRef.current) { - clearTimeout(skipTimeoutRef.current); - } - - const newSkipAmount = skipSide === side ? skipAmount + Math.abs(seconds) : Math.abs(seconds); - setSkipAmount(newSkipAmount); - setSkipSide(side); - setShowSkipIndicator(true); - - const targetTime = side === 'left' - ? Math.max(videoRef.current.currentTime - Math.abs(seconds), 0) - : Math.min(videoRef.current.currentTime + Math.abs(seconds), duration); - - videoRef.current.currentTime = targetTime; - setCurrentTime(targetTime); - - skipTimeoutRef.current = setTimeout(() => { - setShowSkipIndicator(false); - setSkipAmount(0); - setSkipSide(null); - }, 1500); - }, [duration, skipAmount, skipSide, videoRef, skipTimeoutRef, setSkipAmount, setSkipSide, setShowSkipIndicator, setCurrentTime]); - - return { - skipVideo - }; -} diff --git a/components/player/hooks/mobile/useMobileTogglePlay.ts b/components/player/hooks/mobile/useMobileTogglePlay.ts deleted file mode 100644 index b0acf2c..0000000 --- a/components/player/hooks/mobile/useMobileTogglePlay.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useCallback } from 'react'; - -interface UseMobileTogglePlayProps { - videoRef: React.RefObject; - isPlaying: boolean; - isTogglingRef: React.MutableRefObject; - setShowMoreMenu: (show: boolean) => void; - setShowVolumeMenu: (show: boolean) => void; - setShowSpeedMenu: (show: boolean) => void; -} - -export function useMobileTogglePlay({ - videoRef, - isPlaying, - isTogglingRef, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu -}: UseMobileTogglePlayProps) { - return useCallback(async () => { - if (!videoRef.current || isTogglingRef.current) return; - isTogglingRef.current = true; - - try { - if (isPlaying) { - videoRef.current.pause(); - } else { - setShowMoreMenu(false); - setShowVolumeMenu(false); - setShowSpeedMenu(false); - await videoRef.current.play(); - } - } catch (error) { - console.warn('Play/pause error:', error); - } finally { - isTogglingRef.current = false; - } - }, [isPlaying, videoRef, isTogglingRef, setShowMoreMenu, setShowVolumeMenu, setShowSpeedMenu]); -} diff --git a/components/player/hooks/mobile/useMobileUtilities.ts b/components/player/hooks/mobile/useMobileUtilities.ts deleted file mode 100644 index 695653b..0000000 --- a/components/player/hooks/mobile/useMobileUtilities.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useCallback, useEffect } from 'react'; - -interface UseMobileUtilitiesProps { - src: string; - volume: number; - isMuted: boolean; - videoRef: React.RefObject; - setVolume: (volume: number) => void; - setIsMuted: (muted: boolean) => void; - setViewportWidth: (width: number) => void; - setToastMessage: (message: string | null) => void; - setShowToast: (show: boolean) => void; - toastTimeoutRef: React.MutableRefObject; -} - -export function useMobileUtilities({ - src, - volume, - isMuted, - videoRef, - setVolume, - setIsMuted, - setViewportWidth, - setToastMessage, - setShowToast, - toastTimeoutRef -}: UseMobileUtilitiesProps) { - useEffect(() => { - const updateViewportWidth = () => { - setViewportWidth(window.innerWidth); - }; - updateViewportWidth(); - window.addEventListener('resize', updateViewportWidth); - return () => window.removeEventListener('resize', updateViewportWidth); - }, [setViewportWidth]); - - const toggleMute = useCallback(() => { - if (!videoRef.current) return; - if (isMuted) { - videoRef.current.volume = volume; - setIsMuted(false); - } else { - videoRef.current.volume = 0; - setIsMuted(true); - } - }, [videoRef, isMuted, volume, setIsMuted]); - - const showToastNotification = useCallback((message: string) => { - setToastMessage(message); - setShowToast(true); - - if (toastTimeoutRef.current) { - clearTimeout(toastTimeoutRef.current); - } - - toastTimeoutRef.current = setTimeout(() => { - setShowToast(false); - setTimeout(() => setToastMessage(null), 300); - }, 3000); - }, [setToastMessage, setShowToast, toastTimeoutRef]); - - const handleCopyLink = useCallback(async (url?: string) => { - try { - await navigator.clipboard.writeText(url || src); - showToastNotification('链接已复制到剪贴板'); - } catch (error) { - console.error('Copy failed:', error); - showToastNotification('复制失败,请重试'); - } - }, [src, showToastNotification]); - - return { - toggleMute, - showToastNotification, - handleCopyLink - }; -} diff --git a/components/player/hooks/useMobileGestures.ts b/components/player/hooks/useMobileGestures.ts deleted file mode 100644 index 51696c0..0000000 --- a/components/player/hooks/useMobileGestures.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MutableRefObject } from 'react'; -import { useDoubleTap } from '@/lib/hooks/useMobilePlayer'; - -interface UseMobileGesturesProps { - skipVideo: (seconds: number, side: 'left' | 'right') => void; - showSkipIndicator: boolean; - showControls: boolean; - setShowControls: (show: boolean) => void; - controlsTimeoutRef: MutableRefObject; - isPlaying: boolean; - togglePlay: () => void; -} - -export function useMobileGestures({ - skipVideo, - showSkipIndicator, - showControls, - setShowControls, - controlsTimeoutRef, - isPlaying, - togglePlay, -}: UseMobileGesturesProps) { - const { handleTap } = useDoubleTap({ - onDoubleTapLeft: () => skipVideo(10, 'left'), - onDoubleTapRight: () => skipVideo(10, 'right'), - onSkipContinueLeft: () => skipVideo(10, 'left'), - onSkipContinueRight: () => skipVideo(10, 'right'), - isSkipModeActive: showSkipIndicator, - onSingleTap: () => { - if (!showControls) { - setShowControls(true); - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - if (isPlaying) { - controlsTimeoutRef.current = setTimeout(() => { - setShowControls(false); - }, 3000); - } - } else { - togglePlay(); - if (controlsTimeoutRef.current) { - clearTimeout(controlsTimeoutRef.current); - } - if (isPlaying) { - controlsTimeoutRef.current = setTimeout(() => { - setShowControls(false); - }, 3000); - } - } - }, - }); - - return { handleTap }; -} diff --git a/components/player/hooks/useMobilePlayerLogic.ts b/components/player/hooks/useMobilePlayerLogic.ts deleted file mode 100644 index 2998128..0000000 --- a/components/player/hooks/useMobilePlayerLogic.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { useMobilePlaybackControls } from './mobile/useMobilePlaybackControls'; -import { useMobileProgressControls } from './mobile/useMobileProgressControls'; -import { useMobileSkipControls } from './mobile/useMobileSkipControls'; -import { useMobileFullscreenControls } from './mobile/useMobileFullscreenControls'; -import { useMobileMenuControls } from './mobile/useMobileMenuControls'; -import { useMobileUtilities } from './mobile/useMobileUtilities'; -import { - buildPlaybackParams, - buildProgressParams, - buildSkipParams, - buildFullscreenParams, - buildUtilitiesParams, - buildMenuParams, -} from './mobile/mobile-player-params'; - -interface UseMobilePlayerLogicProps { - src: string; - poster?: string; - initialTime: number; - shouldAutoPlay: boolean; - onError?: (error: string) => void; - onTimeUpdate?: (currentTime: number, duration: number) => void; - refs: any; - state: any; -} - -export function useMobilePlayerLogic({ - src, - initialTime, - shouldAutoPlay, - onError, - onTimeUpdate, - refs, - state -}: UseMobilePlayerLogicProps) { - const { - videoRef, - containerRef, - progressBarRef, - controlsTimeoutRef, - skipTimeoutRef, - isDraggingProgressRef, - menuIdleTimeoutRef, - isTogglingRef, - toastTimeoutRef - } = refs; - - const { - isPlaying, setIsPlaying, - currentTime, setCurrentTime, - duration, setDuration, - volume, setVolume, - isMuted, setIsMuted, - isFullscreen, setIsFullscreen, - setShowControls, - setIsLoading, - setPlaybackRate, - showSpeedMenu, setShowSpeedMenu, - showVolumeMenu, setShowVolumeMenu, - showMoreMenu, setShowMoreMenu, - isPiPSupported, setIsPiPSupported, - skipAmount, setSkipAmount, - skipSide, setSkipSide, - setShowSkipIndicator, - wasPlayingBeforeMenu, setWasPlayingBeforeMenu, - setToastMessage, - setShowToast, - setViewportWidth - } = state; - - const playbackControls = useMobilePlaybackControls(buildPlaybackParams({ - videoRef, isPlaying, setIsPlaying, setIsLoading, initialTime, shouldAutoPlay, setDuration, - setCurrentTime, setPlaybackRate, setShowMoreMenu, setShowVolumeMenu, - setShowSpeedMenu, onTimeUpdate, onError, isDraggingProgressRef, isTogglingRef - })); - - const progressControls = useMobileProgressControls(buildProgressParams({ - videoRef, progressBarRef, duration, setCurrentTime, isDraggingProgressRef - })); - - const skipControls = useMobileSkipControls(buildSkipParams({ - videoRef, duration, setCurrentTime, skipAmount, skipSide, setSkipAmount, - setSkipSide, setShowSkipIndicator, skipTimeoutRef - })); - - const fullscreenControls = useMobileFullscreenControls(buildFullscreenParams({ - containerRef, videoRef, isFullscreen, setIsFullscreen, isPiPSupported, setIsPiPSupported - })); - - const utilities = useMobileUtilities(buildUtilitiesParams({ - src, volume, isMuted, videoRef, setVolume, setIsMuted, setViewportWidth, - setToastMessage, setShowToast, toastTimeoutRef - })); - - useMobileMenuControls(buildMenuParams({ - videoRef, isPlaying, showMoreMenu, showVolumeMenu, showSpeedMenu, - wasPlayingBeforeMenu, setShowControls, setShowMoreMenu, setShowVolumeMenu, - setShowSpeedMenu, setWasPlayingBeforeMenu, controlsTimeoutRef, menuIdleTimeoutRef - })); - - return { - skipVideo: skipControls.skipVideo, - togglePlay: playbackControls.togglePlay, - handlePlay: playbackControls.handlePlay, - handlePause: playbackControls.handlePause, - handleTimeUpdateEvent: playbackControls.handleTimeUpdateEvent, - handleLoadedMetadata: playbackControls.handleLoadedMetadata, - handleVideoError: playbackControls.handleVideoError, - handleProgressTouchStart: progressControls.handleProgressTouchStart, - handleProgressTouchMove: progressControls.handleProgressTouchMove, - handleProgressTouchEnd: progressControls.handleProgressTouchEnd, - handleProgressClick: progressControls.handleProgressClick, - toggleMute: utilities.toggleMute, - toggleFullscreen: fullscreenControls.toggleFullscreen, - togglePictureInPicture: fullscreenControls.togglePictureInPicture, - changePlaybackSpeed: playbackControls.changePlaybackSpeed, - showToastNotification: utilities.showToastNotification, - handleCopyLink: (type: 'original' | 'proxy' = 'original') => { - let urlToCopy = src; - - // If user wants original link, strip proxy prefix if present - if (type === 'original') { - if (urlToCopy.includes('/api/proxy?url=')) { - const match = urlToCopy.match(/url=([^&]*)/); - if (match && match[1]) { - urlToCopy = decodeURIComponent(match[1]); - } - } - } - // If user wants proxy link, ensure it has proxy prefix - else if (type === 'proxy') { - if (!urlToCopy.includes('/api/proxy?url=')) { - urlToCopy = `${window.location.origin}/api/proxy?url=${encodeURIComponent(urlToCopy)}`; - } else if (urlToCopy.startsWith('/')) { - // Ensure absolute URL for copy - urlToCopy = `${window.location.origin}${urlToCopy}`; - } - } - - utilities.handleCopyLink(urlToCopy); - }, - formatTime: playbackControls.formatTime - }; -} diff --git a/components/player/hooks/useMobilePlayerState.ts b/components/player/hooks/useMobilePlayerState.ts deleted file mode 100644 index 9f2ec31..0000000 --- a/components/player/hooks/useMobilePlayerState.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useState, useRef } from 'react'; - -export function useMobilePlayerState() { - const videoRef = useRef(null); - const containerRef = useRef(null); - const progressBarRef = useRef(null); - - // Refs for timeouts and tracking - const controlsTimeoutRef = useRef(null); - const skipTimeoutRef = useRef(null); - const isDraggingProgressRef = useRef(false); - const menuIdleTimeoutRef = useRef(null); - const submenuIdleTimeoutRef = useRef(null); - const isTogglingRef = useRef(false); - const toastTimeoutRef = useRef(null); - - // State - const [isPlaying, setIsPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(0); - const [volume, setVolume] = useState(1); - const [isMuted, setIsMuted] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [showControls, setShowControls] = useState(true); - const [isLoading, setIsLoading] = useState(true); - const [playbackRate, setPlaybackRate] = useState(1); - const [showSpeedMenu, setShowSpeedMenu] = useState(false); - const [showVolumeMenu, setShowVolumeMenu] = useState(false); - const [showMoreMenu, setShowMoreMenu] = useState(false); - const [isPiPSupported, setIsPiPSupported] = useState(false); - const [skipAmount, setSkipAmount] = useState(0); - const [skipSide, setSkipSide] = useState<'left' | 'right' | null>(null); - const [showSkipIndicator, setShowSkipIndicator] = useState(false); - const [wasPlayingBeforeMenu, setWasPlayingBeforeMenu] = useState(false); - const [toastMessage, setToastMessage] = useState(null); - const [showToast, setShowToast] = useState(false); - const [viewportWidth, setViewportWidth] = useState(0); - - return { - refs: { - videoRef, - containerRef, - progressBarRef, - controlsTimeoutRef, - skipTimeoutRef, - isDraggingProgressRef, - menuIdleTimeoutRef, - submenuIdleTimeoutRef, - isTogglingRef, - toastTimeoutRef - }, - state: { - isPlaying, setIsPlaying, - currentTime, setCurrentTime, - duration, setDuration, - volume, setVolume, - isMuted, setIsMuted, - isFullscreen, setIsFullscreen, - showControls, setShowControls, - isLoading, setIsLoading, - playbackRate, setPlaybackRate, - showSpeedMenu, setShowSpeedMenu, - showVolumeMenu, setShowVolumeMenu, - showMoreMenu, setShowMoreMenu, - isPiPSupported, setIsPiPSupported, - skipAmount, setSkipAmount, - skipSide, setSkipSide, - showSkipIndicator, setShowSkipIndicator, - wasPlayingBeforeMenu, setWasPlayingBeforeMenu, - toastMessage, setToastMessage, - showToast, setShowToast, - viewportWidth, setViewportWidth - } - }; -} diff --git a/components/player/mobile/CompactControls.tsx b/components/player/mobile/CompactControls.tsx deleted file mode 100644 index dcb7cde..0000000 --- a/components/player/mobile/CompactControls.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import { Icons } from '@/components/ui/Icon'; -import { MobileMoreMenu } from './MobileMoreMenu'; - -interface CompactControlsProps { - isPlaying: boolean; - isFullscreen: boolean; - showMoreMenu: boolean; - isMuted: boolean; - volume: number; - playbackRate: number; - isPiPSupported: boolean; - currentTime: number; - duration: number; - formatTime: (seconds: number) => string; - onTogglePlay: () => void; - onToggleFullscreen: () => void; - onToggleMoreMenu: () => void; - onToggleVolumeMenu: () => void; - onToggleSpeedMenu: () => void; - onTogglePiP: () => void; - onCopyLink: () => void; - iconSize: number; - buttonPadding: string; - controlsGap: string; - textSize: string; -} - -export function CompactControls({ - isPlaying, - isFullscreen, - showMoreMenu, - isMuted, - volume, - playbackRate, - isPiPSupported, - currentTime, - duration, - formatTime, - onTogglePlay, - onToggleFullscreen, - onToggleMoreMenu, - onToggleVolumeMenu, - onToggleSpeedMenu, - onTogglePiP, - onCopyLink, - iconSize, - buttonPadding, - controlsGap, - textSize -}: CompactControlsProps) { - return ( -
-
- - - - {formatTime(currentTime)} / {formatTime(duration)} - -
- -
-
- - - { - onToggleMoreMenu(); - onCopyLink(); - }} - onToggleVolumeMenu={() => { - onToggleMoreMenu(); - onToggleVolumeMenu(); - }} - onToggleSpeedMenu={() => { - onToggleMoreMenu(); - onToggleSpeedMenu(); - }} - onTogglePiP={() => { - onToggleMoreMenu(); - onTogglePiP(); - }} - /> -
- - -
-
- ); -} diff --git a/components/player/mobile/FullControls.tsx b/components/player/mobile/FullControls.tsx deleted file mode 100644 index bccc580..0000000 --- a/components/player/mobile/FullControls.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import { LeftControls } from './controls/LeftControls'; -import { RightControls } from './controls/RightControls'; - -interface FullControlsProps { - isPlaying: boolean; - isFullscreen: boolean; - showVolumeMenu: boolean; - showSpeedMenu: boolean; - showMoreMenu: boolean; - isMuted: boolean; - volume: number; - playbackRate: number; - isPiPSupported: boolean; - isProxied?: boolean; - currentTime: number; - duration: number; - speeds: number[]; - formatTime: (seconds: number) => string; - onTogglePlay: () => void; - onSkipVideo: (seconds: number, side: 'left' | 'right') => void; - onToggleFullscreen: () => void; - onToggleVolumeMenu: () => void; - onToggleSpeedMenu: () => void; - onTogglePiP: () => void; - onToggleMoreMenu: () => void; - onToggleMute: () => void; - onVolumeChange: (volume: number) => void; - onSpeedChange: (speed: number) => void; - onCopyLink: (type?: 'original' | 'proxy') => void; - iconSize: number; - buttonPadding: string; - controlsGap: string; - textSize: string; -} - -export function FullControls({ - isPlaying, - isFullscreen, - showVolumeMenu, - showSpeedMenu, - showMoreMenu, - isMuted, - volume, - playbackRate, - isPiPSupported, - isProxied, - currentTime, - duration, - speeds, - formatTime, - onTogglePlay, - onSkipVideo, - onToggleFullscreen, - onToggleVolumeMenu, - onToggleSpeedMenu, - onTogglePiP, - onToggleMoreMenu, - onToggleMute, - onVolumeChange, - onSpeedChange, - onCopyLink, - iconSize, - buttonPadding, - controlsGap, - textSize -}: FullControlsProps) { - return ( -
- - -
- - -
- ); -} diff --git a/components/player/mobile/MobileControls.tsx b/components/player/mobile/MobileControls.tsx deleted file mode 100644 index 0cee9b4..0000000 --- a/components/player/mobile/MobileControls.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { MobileProgressBar } from './MobileProgressBar'; -import { CompactControls } from './CompactControls'; -import { FullControls } from './FullControls'; - -interface MobileControlsProps { - showControls: boolean; - isCompactLayout: boolean; - isPlaying: boolean; - currentTime: number; - duration: number; - volume: number; - isMuted: boolean; - isFullscreen: boolean; - playbackRate: number; - showMoreMenu: boolean; - showVolumeMenu: boolean; - showSpeedMenu: boolean; - isPiPSupported: boolean; - isProxied?: boolean; - progressBarRef: React.RefObject; - onTogglePlay: () => void; - onSkipVideo: (seconds: number, side: 'left' | 'right') => void; - onToggleMute: () => void; - onToggleFullscreen: () => void; - onToggleMoreMenu: () => void; - onToggleVolumeMenu: () => void; - onToggleSpeedMenu: () => void; - onTogglePiP: () => void; - onVolumeChange: (volume: number) => void; - onSpeedChange: (speed: number) => void; - onCopyLink: (type?: 'original' | 'proxy') => void; - onProgressClick: (e: React.MouseEvent) => void; - onProgressTouchStart: (e: React.TouchEvent) => void; - onProgressTouchMove: (e: React.TouchEvent) => void; - onProgressTouchEnd: (e: React.TouchEvent) => void; - formatTime: (seconds: number) => string; - speeds: number[]; -} - -export function MobileControls(props: MobileControlsProps) { - const { - showControls, - isCompactLayout, - progressBarRef, - currentTime, - duration, - onProgressClick, - onProgressTouchStart, - onProgressTouchMove, - onProgressTouchEnd, - showVolumeMenu, - showSpeedMenu, - isMuted, - volume, - playbackRate, - speeds, - onToggleMute, - onVolumeChange, - onSpeedChange, - onToggleVolumeMenu, - onToggleSpeedMenu - } = props; - - const iconSize = isCompactLayout ? 20 : 22; - const buttonPadding = isCompactLayout ? 'p-2' : 'p-2.5'; - const controlsGap = isCompactLayout ? 'gap-2' : 'gap-3'; - const textSize = isCompactLayout ? 'text-xs' : 'text-sm'; - const controlsPadding = isCompactLayout ? 'px-3 pb-3' : 'px-4 pb-4'; - - return ( -
- - -
- {isCompactLayout ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/components/player/mobile/MobileControlsWrapper.tsx b/components/player/mobile/MobileControlsWrapper.tsx deleted file mode 100644 index d73efa9..0000000 --- a/components/player/mobile/MobileControlsWrapper.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { MobileControls } from './MobileControls'; -import { useMobilePlayerState } from '../hooks/useMobilePlayerState'; -import { useMobilePlayerLogic } from '../hooks/useMobilePlayerLogic'; - -interface MobileControlsWrapperProps { - src: string; - state: ReturnType['state']; - logic: ReturnType; - refs: ReturnType['refs']; -} - -export function MobileControlsWrapper({ src, state, logic, refs }: MobileControlsWrapperProps) { - const { - isPlaying, - currentTime, - duration, - volume, - isMuted, - isFullscreen, - showControls, - playbackRate, - showSpeedMenu, - showVolumeMenu, - showMoreMenu, - isPiPSupported, - viewportWidth, - setShowMoreMenu, - setShowVolumeMenu, - setShowSpeedMenu, - } = state; - - const { - progressBarRef, - videoRef, - } = refs; - - const { - togglePlay, - skipVideo, - toggleMute, - toggleFullscreen, - togglePictureInPicture, - changePlaybackSpeed, - handleCopyLink, - handleProgressClick, - handleProgressTouchStart, - handleProgressTouchMove, - handleProgressTouchEnd, - formatTime, - } = logic; - - const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2]; - const isCompactLayout = viewportWidth < 640; - const isProxied = src.includes('/api/proxy'); // Calculated isProxied - - return ( - setShowMoreMenu(!showMoreMenu)} // Updated to use destructured setter - onToggleVolumeMenu={() => setShowVolumeMenu(!showVolumeMenu)} // Updated to use destructured setter - onToggleSpeedMenu={() => setShowSpeedMenu(!showSpeedMenu)} // Updated to use destructured setter - onTogglePiP={togglePictureInPicture} - onVolumeChange={(newVolume) => { - // Volume change logic - }} // Updated onVolumeChange - onSpeedChange={changePlaybackSpeed} - onCopyLink={handleCopyLink} - onProgressClick={handleProgressClick} - onProgressTouchStart={handleProgressTouchStart} - onProgressTouchMove={handleProgressTouchMove} - onProgressTouchEnd={handleProgressTouchEnd} - formatTime={formatTime} - speeds={speeds} - /> - ); -} diff --git a/components/player/mobile/MobileMoreMenu.tsx b/components/player/mobile/MobileMoreMenu.tsx deleted file mode 100644 index 3ce37e2..0000000 --- a/components/player/mobile/MobileMoreMenu.tsx +++ /dev/null @@ -1,282 +0,0 @@ -'use client'; - -import React from 'react'; -import { Icons } from '@/components/ui/Icon'; -import { usePlayerSettings } from '../hooks/usePlayerSettings'; - -interface MobileMoreMenuProps { - showMoreMenu: boolean; - isMuted: boolean; - volume: number; - playbackRate: number; - isPiPSupported: boolean; - isProxied?: boolean; - onCopyLink: (type?: 'original' | 'proxy') => void; - onToggleVolumeMenu: () => void; - onToggleSpeedMenu: () => void; - onTogglePiP: () => void; -} - -export function MobileMoreMenu({ - showMoreMenu, - isMuted, - volume, - playbackRate, - isPiPSupported, - isProxied = false, - onCopyLink, - onToggleVolumeMenu, - onToggleSpeedMenu, - onTogglePiP -}: MobileMoreMenuProps) { - const { - autoNextEpisode, - autoSkipIntro, - skipIntroSeconds, - autoSkipOutro, - skipOutroSeconds, - showModeIndicator, - setAutoNextEpisode, - setAutoSkipIntro, - setSkipIntroSeconds, - setAutoSkipOutro, - setSkipOutroSeconds, - setShowModeIndicator, - } = usePlayerSettings(); - - if (!showMoreMenu) return null; - - return ( -
-
- {/* Copy Link Option */} - {isProxied ? ( - <> - - - - ) : ( - - )} - -
- - {/* Show Mode Indicator Switch */} -
e.stopPropagation()} - > -
- - 显示模式指示器 -
- -
- - {/* Auto Next Episode Switch */} -
e.stopPropagation()} - > -
- - 自动下一集 -
- -
- - {/* Skip Intro Switch */} -
e.stopPropagation()}> -
-
- - 跳过片头 -
- -
- {/* Expandable Input */} - {autoSkipIntro && ( -
- 时长: - setSkipIntroSeconds(parseInt(e.target.value) || 0)} - className="w-16 px-2 py-1 text-sm text-center bg-white/10 border border-white/20 rounded-[var(--radius-2xl)] text-white focus:outline-none focus:border-[var(--accent-color)] no-spinner" - onClick={(e) => e.stopPropagation()} - /> - -
- )} -
- - {/* Skip Outro Switch */} -
e.stopPropagation()}> -
-
- - 跳过片尾 -
- -
- {/* Expandable Input */} - {autoSkipOutro && ( -
- 剩余: - setSkipOutroSeconds(parseInt(e.target.value) || 0)} - className="w-16 px-2 py-1 text-sm text-center bg-white/10 border border-white/20 rounded-[var(--radius-2xl)] text-white focus:outline-none focus:border-[var(--accent-color)] no-spinner" - onClick={(e) => e.stopPropagation()} - /> - -
- )} -
- -
- - {/* Volume Option */} - - - {/* Speed Option */} - - - {/* PiP Option */} - {isPiPSupported && ( - - )} -
-
- ); -} diff --git a/components/player/mobile/MobileOverlay.tsx b/components/player/mobile/MobileOverlay.tsx deleted file mode 100644 index 2f749bd..0000000 --- a/components/player/mobile/MobileOverlay.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Icons } from '@/components/ui/Icon'; - -interface MobileOverlayProps { - isLoading: boolean; - showToast: boolean; - toastMessage: string | null; -} - -export function MobileOverlay({ - isLoading, - showToast, - toastMessage -}: MobileOverlayProps) { - return ( - <> - {/* Loading Spinner - Glass Effect */} - {isLoading && ( -
-
-
- )} - - {/* Toast Notification */} - {showToast && toastMessage && ( -
-
- - {toastMessage} -
-
- )} - - ); -} diff --git a/components/player/mobile/MobileProgressBar.tsx b/components/player/mobile/MobileProgressBar.tsx deleted file mode 100644 index a5d3113..0000000 --- a/components/player/mobile/MobileProgressBar.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { RefObject } from 'react'; - -interface MobileProgressBarProps { - progressBarRef: RefObject; - - currentTime: number; - duration: number; - onProgressClick: (e: React.MouseEvent) => void; - onProgressTouchStart: (e: React.TouchEvent) => void; - onProgressTouchMove: (e: React.TouchEvent) => void; - onProgressTouchEnd: (e: React.TouchEvent) => void; -} - -export function MobileProgressBar({ - progressBarRef, - currentTime, - duration, - onProgressClick, - onProgressTouchStart, - onProgressTouchMove, - onProgressTouchEnd -}: MobileProgressBarProps) { - return ( -
-
-
-
-
-
-
- ); -} diff --git a/components/player/mobile/MobileSkipIndicator.tsx b/components/player/mobile/MobileSkipIndicator.tsx deleted file mode 100644 index 69666b5..0000000 --- a/components/player/mobile/MobileSkipIndicator.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -interface MobileSkipIndicatorProps { - showSkipIndicator: boolean; - skipSide: 'left' | 'right' | null; - skipAmount: number; -} - -export function MobileSkipIndicator({ - showSkipIndicator, - skipSide, - skipAmount -}: MobileSkipIndicatorProps) { - if (!showSkipIndicator || !skipSide) return null; - - return ( -
-
- {skipSide === 'left' ? '-' : '+'}{skipAmount} -
-
- ); -} diff --git a/components/player/mobile/MobileSpeedMenu.tsx b/components/player/mobile/MobileSpeedMenu.tsx deleted file mode 100644 index d6965ae..0000000 --- a/components/player/mobile/MobileSpeedMenu.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -interface MobileSpeedMenuProps { - showSpeedMenu: boolean; - isCompactLayout: boolean; - playbackRate: number; - speeds: number[]; - onSpeedChange: (speed: number) => void; - onClose: () => void; -} - -export function MobileSpeedMenu({ - showSpeedMenu, - isCompactLayout, - playbackRate, - speeds, - onSpeedChange, - onClose -}: MobileSpeedMenuProps) { - if (!showSpeedMenu) return null; - - if (isCompactLayout) { - return ( -
-
- {speeds.map((speed) => ( - - ))} -
- -
- ); - } - - return ( -
-
- {speeds.map((speed) => ( - - ))} -
-
- ); -} diff --git a/components/player/mobile/MobileVolumeMenu.tsx b/components/player/mobile/MobileVolumeMenu.tsx deleted file mode 100644 index cff7648..0000000 --- a/components/player/mobile/MobileVolumeMenu.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { Icons } from '@/components/ui/Icon'; - -interface MobileVolumeMenuProps { - showVolumeMenu: boolean; - isCompactLayout: boolean; - isMuted: boolean; - volume: number; - onToggleMute: () => void; - onVolumeChange: (volume: number) => void; - onClose: () => void; -} - -export function MobileVolumeMenu({ - showVolumeMenu, - isCompactLayout, - isMuted, - volume, - onToggleMute, - onVolumeChange, - onClose -}: MobileVolumeMenuProps) { - if (!showVolumeMenu) return null; - - if (isCompactLayout) { - return ( -
-
- -
- onVolumeChange(parseFloat(e.target.value) / 100)} - className="w-full h-1 bg-white/30 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white" - /> -
- - {Math.round((isMuted ? 0 : volume) * 100)} - - -
-
- ); - } - - return ( -
-
- - - { - e.stopPropagation(); - onVolumeChange(parseFloat(e.target.value)); - }} - className="h-24 w-1 bg-white/30 rounded-full appearance-none cursor-pointer [writing-mode:vertical-lr] [direction:rtl] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white" - style={{ WebkitAppearance: 'slider-vertical' }} - /> - - - {Math.round((isMuted ? 0 : volume) * 100)} - -
-
- ); -} diff --git a/components/player/mobile/controls/LeftControls.tsx b/components/player/mobile/controls/LeftControls.tsx deleted file mode 100644 index ae0fc8a..0000000 --- a/components/player/mobile/controls/LeftControls.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import { Icons } from '@/components/ui/Icon'; -import { MobileVolumeMenu } from '../MobileVolumeMenu'; - -interface LeftControlsProps { - isPlaying: boolean; - onTogglePlay: () => void; - onSkipVideo: (seconds: number, side: 'left' | 'right') => void; - isMuted: boolean; - volume: number; - showVolumeMenu: boolean; - onToggleVolumeMenu: () => void; - onToggleMute: () => void; - onVolumeChange: (volume: number) => void; - currentTime: number; - duration: number; - formatTime: (seconds: number) => string; - iconSize: number; - buttonPadding: string; - textSize: string; - controlsGap: string; -} - -export function LeftControls({ - isPlaying, - onTogglePlay, - onSkipVideo, - isMuted, - volume, - showVolumeMenu, - onToggleVolumeMenu, - onToggleMute, - onVolumeChange, - currentTime, - duration, - formatTime, - iconSize, - buttonPadding, - textSize, - controlsGap, -}: LeftControlsProps) { - return ( -
- - - - - - -
- - - -
- - - {formatTime(currentTime)} / {formatTime(duration)} - -
- ); -} diff --git a/components/player/mobile/controls/RightControls.tsx b/components/player/mobile/controls/RightControls.tsx deleted file mode 100644 index 29abee4..0000000 --- a/components/player/mobile/controls/RightControls.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from 'react'; -import { Icons } from '@/components/ui/Icon'; -import { MobileSpeedMenu } from '../MobileSpeedMenu'; -import { MobileMoreMenu } from '../MobileMoreMenu'; - -interface RightControlsProps { - playbackRate: number; - showSpeedMenu: boolean; - showMoreMenu: boolean; - onToggleSpeedMenu: () => void; - speeds: number[]; - onSpeedChange: (speed: number) => void; - isPiPSupported: boolean; - isProxied?: boolean; - onTogglePiP: () => void; - onToggleMoreMenu: () => void; - onToggleVolumeMenu: () => void; - onCopyLink: (type?: 'original' | 'proxy') => void; - isMuted: boolean; - volume: number; - isFullscreen: boolean; - onToggleFullscreen: () => void; - iconSize: number; - buttonPadding: string; - textSize: string; - controlsGap: string; -} - -export function RightControls({ - playbackRate, - showSpeedMenu, - showMoreMenu, - onToggleSpeedMenu, - speeds, - onSpeedChange, - isPiPSupported, - isProxied, - onTogglePiP, - onToggleMoreMenu, - onToggleVolumeMenu, - onCopyLink, - isMuted, - volume, - isFullscreen, - onToggleFullscreen, - iconSize, - buttonPadding, - textSize, - controlsGap, -}: RightControlsProps) { - return ( -
-
- - - -
- - {isPiPSupported && ( - - )} - -
- - - { - onToggleMoreMenu(); - onCopyLink(type); - }} - onToggleVolumeMenu={() => { - onToggleMoreMenu(); - onToggleVolumeMenu(); - }} - onToggleSpeedMenu={() => { - onToggleMoreMenu(); - onToggleSpeedMenu(); - }} - onTogglePiP={() => { - onToggleMoreMenu(); - onTogglePiP(); - }} - /> -
- - -
- ); -} diff --git a/components/ui/icons/utility-icons.tsx b/components/ui/icons/utility-icons.tsx index 4ee228a..c154a7c 100644 --- a/components/ui/icons/utility-icons.tsx +++ b/components/ui/icons/utility-icons.tsx @@ -157,4 +157,11 @@ export const UtilityIcons = { ), + MoreHorizontal: ({ className = "", size = 24 }: IconProps) => ( + + + + + + ), };