fix: Adjust desktop player menus to portal into a specified container for improved positioning.

This commit is contained in:
kuekhaoyang
2025-12-28 18:16:35 +08:00
parent 6d5267be79
commit deb55a40fe
8 changed files with 84 additions and 32 deletions
+2
View File
@@ -147,6 +147,8 @@ export function DesktopVideoPlayer({
onSpeedChange={logic.changePlaybackSpeed}
onSpeedMenuMouseEnter={logic.clearSpeedMenuTimeout}
onSpeedMenuMouseLeave={logic.startSpeedMenuTimeout}
// Portal container
containerRef={containerRef}
/>
<DesktopControlsWrapper
+18 -12
View File
@@ -13,6 +13,7 @@ interface DesktopMoreMenuProps {
onMouseEnter: () => void;
onMouseLeave: () => void;
onCopyLink: (type?: 'original' | 'proxy') => void;
containerRef: React.RefObject<HTMLDivElement | null>;
}
export function DesktopMoreMenu({
@@ -21,7 +22,8 @@ export function DesktopMoreMenu({
onToggleMoreMenu,
onMouseEnter,
onMouseLeave,
onCopyLink
onCopyLink,
containerRef
}: DesktopMoreMenuProps) {
const {
autoNextEpisode,
@@ -42,21 +44,25 @@ export function DesktopMoreMenu({
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0 });
React.useEffect(() => {
if (showMoreMenu && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
if (showMoreMenu && buttonRef.current && containerRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + 10, // 10px spacing
left: rect.left
top: buttonRect.bottom - containerRect.top + 10,
left: buttonRect.left - containerRect.left
});
}
}, [showMoreMenu]);
}, [showMoreMenu, containerRef]);
const handleToggle = () => {
if (!showMoreMenu && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
if (!showMoreMenu && buttonRef.current && containerRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + 10, // 10px spacing
left: rect.left
top: buttonRect.bottom - containerRect.top + 10,
left: buttonRect.left - containerRect.left
});
}
onToggleMoreMenu();
@@ -64,7 +70,7 @@ export function DesktopMoreMenu({
const MenuContent = (
<div
className="fixed z-[9999] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-2 min-w-[220px] animate-in fade-in zoom-in-95 duration-200"
className="absolute z-[9999] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-2 w-fit min-w-[220px] animate-in fade-in zoom-in-95 duration-200"
style={{
top: menuPosition.top,
left: menuPosition.left,
@@ -237,7 +243,7 @@ export function DesktopMoreMenu({
</button>
{/* More Menu Dropdown (Portal) */}
{showMoreMenu && typeof document !== 'undefined' && createPortal(MenuContent, document.body)}
{showMoreMenu && typeof document !== 'undefined' && createPortal(MenuContent, containerRef.current || document.body)}
</div>
);
}
+7 -3
View File
@@ -33,6 +33,7 @@ interface DesktopOverlayProps {
onSpeedChange: (speed: number) => void;
onSpeedMenuMouseEnter: () => void;
onSpeedMenuMouseLeave: () => void;
containerRef: React.RefObject<HTMLDivElement | null>;
}
export function DesktopOverlay({
@@ -62,7 +63,8 @@ export function DesktopOverlay({
onToggleSpeedMenu,
onSpeedChange,
onSpeedMenuMouseEnter,
onSpeedMenuMouseLeave
onSpeedMenuMouseLeave,
containerRef
}: DesktopOverlayProps) {
// Show navigation buttons when controls are visible or when paused (controls usually show when paused anyway)
const showNavButtons = showControls || !isPlaying;
@@ -70,7 +72,7 @@ export function DesktopOverlay({
return (
<>
{/* More Menu (Top Left) */}
<div className={`absolute top-6 left-6 z-30 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`} style={{ pointerEvents: showControls ? 'auto' : 'none' }}>
<div className={`absolute top-6 left-6 z-50 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`} style={{ pointerEvents: showControls ? 'auto' : 'none' }}>
<DesktopMoreMenu
showMoreMenu={showMoreMenu}
isProxied={isProxied}
@@ -78,11 +80,12 @@ export function DesktopOverlay({
onMouseEnter={onMoreMenuMouseEnter}
onMouseLeave={onMoreMenuMouseLeave}
onCopyLink={onCopyLink}
containerRef={containerRef}
/>
</div>
{/* Speed Menu (Top Right) */}
<div className={`absolute top-6 right-6 z-30 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`} style={{ pointerEvents: showControls ? 'auto' : 'none' }}>
<div className={`absolute top-6 right-6 z-50 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`} style={{ pointerEvents: showControls ? 'auto' : 'none' }}>
<DesktopSpeedMenu
showSpeedMenu={showSpeedMenu}
playbackRate={playbackRate}
@@ -91,6 +94,7 @@ export function DesktopOverlay({
onToggleSpeedMenu={onToggleSpeedMenu}
onMouseEnter={onSpeedMenuMouseEnter}
onMouseLeave={onSpeedMenuMouseLeave}
containerRef={containerRef}
/>
</div>
@@ -22,6 +22,7 @@ interface DesktopOverlayWrapperProps {
onSpeedChange: (speed: number) => void;
onSpeedMenuMouseEnter: () => void;
onSpeedMenuMouseLeave: () => void;
containerRef: React.RefObject<HTMLDivElement | null>;
}
export function DesktopOverlayWrapper({
@@ -42,7 +43,8 @@ export function DesktopOverlayWrapper({
onToggleSpeedMenu,
onSpeedChange,
onSpeedMenuMouseEnter,
onSpeedMenuMouseLeave
onSpeedMenuMouseLeave,
containerRef
}: DesktopOverlayWrapperProps) {
const {
isLoading,
@@ -86,6 +88,7 @@ export function DesktopOverlayWrapper({
onSpeedChange={onSpeedChange}
onSpeedMenuMouseEnter={onSpeedMenuMouseEnter}
onSpeedMenuMouseLeave={onSpeedMenuMouseLeave}
containerRef={containerRef}
/>
);
}
+19 -13
View File
@@ -9,6 +9,7 @@ interface DesktopSpeedMenuProps {
onToggleSpeedMenu: () => void;
onMouseEnter: () => void;
onMouseLeave: () => void;
containerRef: React.RefObject<HTMLDivElement | null>;
}
export function DesktopSpeedMenu({
@@ -18,27 +19,32 @@ export function DesktopSpeedMenu({
onSpeedChange,
onToggleSpeedMenu,
onMouseEnter,
onMouseLeave
onMouseLeave,
containerRef
}: DesktopSpeedMenuProps) {
const buttonRef = React.useRef<HTMLButtonElement>(null);
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0 });
React.useEffect(() => {
if (showSpeedMenu && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
if (showSpeedMenu && buttonRef.current && containerRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + 10,
left: rect.right // Align with right edge
top: buttonRect.bottom - containerRect.top + 10,
left: buttonRect.right - containerRect.left
});
}
}, [showSpeedMenu]);
}, [showSpeedMenu, containerRef]);
const handleToggle = () => {
if (!showSpeedMenu && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
if (!showSpeedMenu && buttonRef.current && containerRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + 10,
left: rect.right // Align with right edge
top: buttonRect.bottom - containerRect.top + 10,
left: buttonRect.right - containerRect.left
});
}
onToggleSpeedMenu();
@@ -47,7 +53,7 @@ export function DesktopSpeedMenu({
const MenuContent = (
<div
className="fixed z-[9999] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-2 min-w-[5rem]"
className="absolute z-[9999] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-1.5 w-fit min-w-[4.5rem]"
style={{
top: menuPosition.top,
left: menuPosition.left,
@@ -60,7 +66,7 @@ export function DesktopSpeedMenu({
<button
key={speed}
onClick={() => onSpeedChange(speed)}
className={`w-full px-3 py-2 rounded-[var(--radius-2xl)] text-sm font-medium transition-colors ${playbackRate === speed
className={`w-full px-4 py-1.5 rounded-[var(--radius-2xl)] text-sm font-medium transition-colors ${playbackRate === speed
? 'bg-[var(--accent-color)] text-white'
: 'text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)]'
}`}
@@ -85,7 +91,7 @@ export function DesktopSpeedMenu({
</button>
{/* Speed Menu (Portal) */}
{showSpeedMenu && typeof document !== 'undefined' && createPortal(MenuContent, document.body)}
{showSpeedMenu && typeof document !== 'undefined' && createPortal(MenuContent, containerRef.current || document.body)}
</div>
);
}
@@ -47,6 +47,15 @@ export function useFullscreenControls({
// Fallback for browsers that only support fullscreen on video element (like some car browsers)
(videoRef.current as any).webkitEnterFullscreen();
}
// Lock orientation to landscape on mobile devices if supported
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.lock) {
try {
await (window.screen as any).orientation.lock('landscape');
} catch (e) {
console.warn('Orientation lock failed:', e);
}
}
} catch (error) {
console.warn('Fullscreen request failed, trying fallback:', error);
// Last ditch effort: try native video fullscreen if container failed
@@ -69,6 +78,15 @@ export function useFullscreenControls({
} else if ((document as any).msExitFullscreen) {
await (document as any).msExitFullscreen();
}
// Unlock orientation when exiting fullscreen
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.unlock) {
try {
(window.screen as any).orientation.unlock();
} catch (e) {
console.warn('Orientation unlock failed:', e);
}
}
} catch (error) {
console.error('Failed to exit fullscreen:', error);
}
@@ -84,6 +102,19 @@ export function useFullscreenControls({
(document as any).msFullscreenElement
);
setIsFullscreen(isInFullscreen);
// Double check orientation lock/unlock on change
if (isInFullscreen) {
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.lock) {
(window.screen as any).orientation.lock('landscape').catch(() => { });
}
} else {
if (window.screen && (window.screen as any).orientation && (window.screen as any).orientation.unlock) {
try {
(window.screen as any).orientation.unlock();
} catch (e) { }
}
}
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "kvideo",
"version": "3.7.2",
"version": "3.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvideo",
"version": "3.7.2",
"version": "3.7.5",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "3.7.2",
"version": "3.7.5",
"private": true,
"scripts": {
"dev": "next dev",