mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
fix: Adjust desktop player menus to portal into a specified container for improved positioning.
This commit is contained in:
@@ -147,6 +147,8 @@ export function DesktopVideoPlayer({
|
||||
onSpeedChange={logic.changePlaybackSpeed}
|
||||
onSpeedMenuMouseEnter={logic.clearSpeedMenuTimeout}
|
||||
onSpeedMenuMouseLeave={logic.startSpeedMenuTimeout}
|
||||
// Portal container
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
|
||||
<DesktopControlsWrapper
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "3.7.2",
|
||||
"version": "3.7.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user