feat: Implement PWA support and enhance desktop player menu positioning with dynamic space awareness.

This commit is contained in:
kuekhaoyang
2026-02-03 20:18:38 +08:00
parent a68be420af
commit 56004edb29
3 changed files with 105 additions and 20 deletions
+13
View File
@@ -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
+66 -20
View File
@@ -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}
+26
View File
@@ -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
}