diff --git a/components/iptv/IPTVPlayer.tsx b/components/iptv/IPTVPlayer.tsx index 5c508f0..3b9cef2 100644 --- a/components/iptv/IPTVPlayer.tsx +++ b/components/iptv/IPTVPlayer.tsx @@ -13,6 +13,7 @@ import { Icons } from '@/components/ui/Icon'; import type { M3UChannel } from '@/lib/utils/m3u-parser'; import type { IPTVSource } from '@/lib/store/iptv-store'; import { settingsStore, DEFAULT_SEEK_STEP_SECONDS } from '@/lib/store/settings-store'; +import { shouldHidePlayerCursor } from '@/lib/player/cursor-visibility'; const HLS_LIVE_CONFIG: Partial = { enableWorker: true, @@ -495,6 +496,13 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe return Math.max(0, Math.min(100, (currentTime / duration) * 100)); }, [currentTime, duration, seekWindow]); + const shouldHideCursor = shouldHidePlayerCursor({ + isFullscreen, + isPlaying, + showControls, + hasInteractiveOverlay: showSidebar || showVolumeSlider || Boolean(error), + }); + const toggleFullscreen = async () => { if (!containerRef.current) return; if (document.fullscreenElement) { @@ -781,6 +789,7 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
{ if ((e.target as HTMLElement).closest('[data-controls]') || (e.target as HTMLElement).closest('[data-sidebar]')) return; diff --git a/components/player/DesktopVideoPlayer.tsx b/components/player/DesktopVideoPlayer.tsx index 7d83e4a..1129717 100644 --- a/components/player/DesktopVideoPlayer.tsx +++ b/components/player/DesktopVideoPlayer.tsx @@ -16,6 +16,7 @@ import { useIsIOS, useIsMobile } from '@/lib/hooks/mobile/useDeviceDetection'; import { useDoubleTap } from '@/lib/hooks/mobile/useDoubleTap'; import { settingsStore, DEFAULT_SEEK_STEP_SECONDS } from '@/lib/store/settings-store'; import { premiumModeSettingsStore } from '@/lib/store/premium-mode-settings'; +import { shouldHidePlayerCursor } from '@/lib/player/cursor-visibility'; import './web-fullscreen.css'; type WebFullscreenSize = 'full' | 'large' | 'focused'; @@ -312,6 +313,18 @@ export function DesktopVideoPlayer({ }; }, [data.fullscreenMode, shouldForceLandscape, viewportMetrics, webFullscreenSize]); + const shouldHideCursor = shouldHidePlayerCursor({ + isFullscreen: data.isFullscreen, + isPlaying: data.isPlaying, + showControls: data.showControls, + hasInteractiveOverlay: data.showSpeedMenu || data.showMoreMenu || data.showVolumeBar, + }); + + const containerStyle = React.useMemo(() => ({ + ...(webFullscreenStyle ?? {}), + cursor: shouldHideCursor ? 'none' : undefined, + }), [webFullscreenStyle, shouldHideCursor]); + const stageClassName = data.fullscreenMode === 'window' ? 'kvideo-stage kvideo-web-fullscreen-stage' : 'kvideo-stage absolute inset-0'; @@ -344,7 +357,7 @@ export function DesktopVideoPlayer({ ref={containerRef} className={`kvideo-container relative aspect-video bg-black group ${data.fullscreenMode === 'window' ? 'is-web-fullscreen' : '' } ${shouldForceLandscape ? 'force-landscape' : ''} ${isTopAlignedWebFullscreen ? 'top-align-stage' : ''} overflow-hidden rounded-none sm:rounded-[var(--radius-2xl)]`} - style={webFullscreenStyle} + style={containerStyle} onMouseMove={() => { handleMouseMove(); }} onMouseLeave={() => isPlaying && setShowControls(false)} > diff --git a/lib/player/cursor-visibility.ts b/lib/player/cursor-visibility.ts new file mode 100644 index 0000000..741d71c --- /dev/null +++ b/lib/player/cursor-visibility.ts @@ -0,0 +1,15 @@ +export interface PlayerCursorVisibilityState { + isFullscreen: boolean; + isPlaying: boolean; + showControls: boolean; + hasInteractiveOverlay?: boolean; +} + +export function shouldHidePlayerCursor({ + isFullscreen, + isPlaying, + showControls, + hasInteractiveOverlay = false, +}: PlayerCursorVisibilityState): boolean { + return isFullscreen && isPlaying && !showControls && !hasInteractiveOverlay; +} diff --git a/tests/player-cursor-visibility.test.ts b/tests/player-cursor-visibility.test.ts new file mode 100644 index 0000000..7ce9556 --- /dev/null +++ b/tests/player-cursor-visibility.test.ts @@ -0,0 +1,44 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { shouldHidePlayerCursor } from '@/lib/player/cursor-visibility'; + +test('shouldHidePlayerCursor hides the cursor during unobstructed fullscreen playback', () => { + assert.equal(shouldHidePlayerCursor({ + isFullscreen: true, + isPlaying: true, + showControls: false, + }), true); +}); + +test('shouldHidePlayerCursor keeps the cursor visible outside fullscreen', () => { + assert.equal(shouldHidePlayerCursor({ + isFullscreen: false, + isPlaying: true, + showControls: false, + }), false); +}); + +test('shouldHidePlayerCursor keeps the cursor visible while paused', () => { + assert.equal(shouldHidePlayerCursor({ + isFullscreen: true, + isPlaying: false, + showControls: false, + }), false); +}); + +test('shouldHidePlayerCursor keeps the cursor visible while controls are shown', () => { + assert.equal(shouldHidePlayerCursor({ + isFullscreen: true, + isPlaying: true, + showControls: true, + }), false); +}); + +test('shouldHidePlayerCursor keeps the cursor visible while an interactive overlay is open', () => { + assert.equal(shouldHidePlayerCursor({ + isFullscreen: true, + isPlaying: true, + showControls: false, + hasInteractiveOverlay: true, + }), false); +});