mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Implement PWA support and enhance desktop player menu positioning with dynamic space awareness.
This commit is contained in:
@@ -77,6 +77,19 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<head>
|
||||
{/* PWA Manifest */}
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
{/* Apple PWA Support */}
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="KVideo" />
|
||||
<link rel="apple-touch-icon" href="/icon.png" />
|
||||
{/* Theme Color (for browser address bar) */}
|
||||
<meta name="theme-color" content="#000000" />
|
||||
{/* Mobile viewport */}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
suppressHydrationWarning
|
||||
|
||||
@@ -48,7 +48,8 @@ export function DesktopMoreMenu({
|
||||
} = usePlayerSettings();
|
||||
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0 });
|
||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
||||
const [menuPosition, setMenuPosition] = React.useState({ top: 0, left: 0, maxHeight: 'none', openUpward: false });
|
||||
const [isAdFilterOpen, setAdFilterOpen] = React.useState(false);
|
||||
|
||||
const AD_FILTER_LABELS: Record<string, string> = {
|
||||
@@ -62,23 +63,61 @@ export function DesktopMoreMenu({
|
||||
|
||||
React.useEffect(() => {
|
||||
const updateFullscreen = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement);
|
||||
// Check both native fullscreen and window fullscreen (CSS-based)
|
||||
const nativeFullscreen = !!document.fullscreenElement;
|
||||
const windowFullscreen = containerRef.current?.closest('.is-web-fullscreen') !== null;
|
||||
setIsFullscreen(nativeFullscreen || windowFullscreen);
|
||||
};
|
||||
document.addEventListener('fullscreenchange', updateFullscreen);
|
||||
// Also check periodically for window fullscreen changes (CSS class based)
|
||||
const interval = setInterval(updateFullscreen, 500);
|
||||
updateFullscreen();
|
||||
return () => document.removeEventListener('fullscreenchange', updateFullscreen);
|
||||
}, []);
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', updateFullscreen);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [containerRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showMoreMenu && buttonRef.current && containerRef.current) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
// Calculate menu position with available space awareness
|
||||
const calculateMenuPosition = React.useCallback(() => {
|
||||
if (!buttonRef.current || !containerRef.current) return;
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Space available below and above the button
|
||||
const spaceBelow = viewportHeight - buttonRect.bottom - 20; // 20px margin
|
||||
const spaceAbove = buttonRect.top - containerRect.top - 20;
|
||||
|
||||
// Estimate menu height (or use actual if already rendered)
|
||||
const estimatedMenuHeight = 450; // approximate height of menu
|
||||
const actualMenuHeight = menuRef.current?.offsetHeight || estimatedMenuHeight;
|
||||
|
||||
// Determine if we should open upward
|
||||
const openUpward = spaceBelow < Math.min(actualMenuHeight, 300) && spaceAbove > spaceBelow;
|
||||
|
||||
// Calculate max-height based on available space
|
||||
const maxHeight = openUpward
|
||||
? Math.min(spaceAbove, actualMenuHeight)
|
||||
: Math.min(spaceBelow, viewportHeight * 0.7);
|
||||
|
||||
if (openUpward) {
|
||||
setMenuPosition({
|
||||
top: buttonRect.top - containerRect.top - 10, // Position above button
|
||||
left: buttonRect.left - containerRect.left,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
openUpward: true
|
||||
});
|
||||
} else {
|
||||
setMenuPosition({
|
||||
top: buttonRect.bottom - containerRect.top + 10,
|
||||
left: buttonRect.left - containerRect.left
|
||||
left: buttonRect.left - containerRect.left,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
openUpward: false
|
||||
});
|
||||
}
|
||||
}, [showMoreMenu, containerRef]);
|
||||
}, [containerRef]);
|
||||
|
||||
// Auto-close menu on scroll
|
||||
React.useEffect(() => {
|
||||
@@ -92,25 +131,32 @@ export function DesktopMoreMenu({
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [showMoreMenu, onToggleMoreMenu]);
|
||||
|
||||
// Recalculate position when menu opens
|
||||
React.useEffect(() => {
|
||||
if (showMoreMenu) {
|
||||
calculateMenuPosition();
|
||||
// Recalculate after a small delay to get actual menu height
|
||||
const timer = setTimeout(calculateMenuPosition, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showMoreMenu, calculateMenuPosition]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!showMoreMenu && buttonRef.current && containerRef.current) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
setMenuPosition({
|
||||
top: buttonRect.bottom - containerRect.top + 10,
|
||||
left: buttonRect.left - containerRect.left
|
||||
});
|
||||
if (!showMoreMenu) {
|
||||
calculateMenuPosition();
|
||||
}
|
||||
onToggleMoreMenu();
|
||||
};
|
||||
|
||||
const MenuContent = (
|
||||
<div
|
||||
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 sm:p-2 w-fit min-w-[200px] sm:min-w-[240px] animate-in fade-in zoom-in-95 duration-200 ${isFullscreen ? 'max-h-[70vh] overflow-y-auto' : ''
|
||||
}`}
|
||||
ref={menuRef}
|
||||
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 sm:p-2 w-fit min-w-[200px] sm:min-w-[240px] animate-in fade-in zoom-in-95 duration-200 overflow-y-auto`}
|
||||
style={{
|
||||
top: `${menuPosition.top}px`,
|
||||
top: menuPosition.openUpward ? 'auto' : `${menuPosition.top}px`,
|
||||
bottom: menuPosition.openUpward ? `calc(100% - ${menuPosition.top}px + 10px)` : 'auto',
|
||||
left: `${menuPosition.left}px`,
|
||||
maxHeight: isFullscreen ? menuPosition.maxHeight : 'none',
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "KVideo - 视频聚合平台",
|
||||
"short_name": "KVideo",
|
||||
"description": "多源视频聚合搜索和播放平台",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
"entertainment",
|
||||
"video"
|
||||
],
|
||||
"lang": "zh-CN",
|
||||
"dir": "ltr",
|
||||
"scope": "/",
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
Reference in New Issue
Block a user