diff --git a/src/app/globals.css b/src/app/globals.css index b266c96..b0db077 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -99,19 +99,3 @@ body { *::-webkit-scrollbar { display: none; /* Chrome, Safari, Opera */ } - -/* 隐藏 Artplayer 顶部提示 */ -.art-notice { - display: none !important; -} - -.art-poster { - background-size: contain !important; /* 使图片完整展示 */ - background-position: center center !important; /* 居中显示 */ - background-repeat: no-repeat !important; /* 防止重复 */ - background-color: #000 !important; /* 其余区域填充为黑色 */ -} - -.art-video-player .art-layers { - z-index: 100 !important; -} diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index b19ed81..dbb2e44 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -10,7 +10,7 @@ import { import { Heart } from 'lucide-react'; import { useSearchParams } from 'next/navigation'; import { Suspense } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import React from 'react'; import 'vidstack/styles/defaults.css'; @@ -48,6 +48,7 @@ function PlayPageClient() { const searchParams = useSearchParams(); // @ts-ignore const playerRef = useRef(null); + const playerContainerRef = useRef(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -100,6 +101,7 @@ function PlayPageClient() { ? window.matchMedia('(orientation: portrait)').matches : true ); + const [isFullscreen, setIsFullscreen] = useState(false); // 长按三倍速相关状态 const [isLongPressing, setIsLongPressing] = useState(false); @@ -122,31 +124,23 @@ function PlayPageClient() { useEffect(() => { currentSourceRef.current = currentSource; currentIdRef.current = currentId; - }, [currentSource, currentId]); - - useEffect(() => { - videoTitleRef.current = videoTitle; - }, [videoTitle]); - - // 保持引用最新 - useEffect(() => { - currentEpisodeIndexRef.current = currentEpisodeIndex; - }, [currentEpisodeIndex]); - - useEffect(() => { detailRef.current = detail; - }, [detail]); + currentEpisodeIndexRef.current = currentEpisodeIndex; + videoTitleRef.current = videoTitle; + }, [currentSource, currentId, detail, currentEpisodeIndex, videoTitle]); + + // 解决 iOS Safari 100vh 不准确的问题:将视口高度写入 CSS 变量 --vh + const setVH = useCallback(() => { + if (typeof window !== 'undefined') { + document.documentElement.style.setProperty( + '--vh', + `${window.innerHeight * 0.01}px` + ); + } + }, []); // 解决 iOS Safari 100vh 不准确的问题:将视口高度写入 CSS 变量 --vh useEffect(() => { - const setVH = () => { - if (typeof window !== 'undefined') { - document.documentElement.style.setProperty( - '--vh', - `${window.innerHeight * 0.01}px` - ); - } - }; setVH(); window.addEventListener('resize', setVH); window.addEventListener('orientationchange', setVH); @@ -154,7 +148,7 @@ function PlayPageClient() { window.removeEventListener('resize', setVH); window.removeEventListener('orientationchange', setVH); }; - }, []); + }, [setVH]); // 根据 detail 和集数索引更新视频地址(仅当地址真正变化时) const updateVideoUrl = ( @@ -172,6 +166,7 @@ function PlayPageClient() { const newUrl = detailData?.episodes[episodeIndex] || ''; if (newUrl !== videoUrl) { setVideoUrl(newUrl); + playerContainerRef.current?.focus(); } }; @@ -293,7 +288,6 @@ function PlayPageClient() { }; initFromHistory(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 播放器事件处理 @@ -554,6 +548,7 @@ function PlayPageClient() { // 处理播放源面板展开 const handleSourcePanelOpen = () => { setShowSourcePanel(true); + playerContainerRef.current?.focus(); // 只在第一次展开时搜索 if (videoTitle && !hasSearchedRef.current) { handleSearch(videoTitle); @@ -592,8 +587,10 @@ function PlayPageClient() { if (detailRef.current && currentEpisodeIndexRef.current > 0) { handlePreviousEpisode(); displayShortcutHint('上一集', 'left'); - e.preventDefault(); + } else { + displayShortcutHint('已经是第一集了', 'error'); } + e.preventDefault(); } // Alt + 右箭头 = 下一集 @@ -603,8 +600,10 @@ function PlayPageClient() { if (d && idx < d.episodes.length - 1) { handleNextEpisode(); displayShortcutHint('下一集', 'right'); - e.preventDefault(); + } else { + displayShortcutHint('已经是最后一集了', 'error'); } + e.preventDefault(); } if (!playerRef.current) return; @@ -615,8 +614,8 @@ function PlayPageClient() { if (player.currentTime > 5) { player.currentTime -= 10; displayShortcutHint('快退', 'left'); - e.preventDefault(); } + e.preventDefault(); } // 右箭头 = 快进 @@ -624,8 +623,8 @@ function PlayPageClient() { if (player.currentTime < player.duration - 5) { player.currentTime += 10; displayShortcutHint('快进', 'right'); - e.preventDefault(); } + e.preventDefault(); } // 上箭头 = 音量+ @@ -633,8 +632,10 @@ function PlayPageClient() { if (player.volume < 1) { player.volume += 0.1; displayShortcutHint(`音量 ${Math.round(player.volume * 100)}`, 'up'); - e.preventDefault(); + } else { + displayShortcutHint('音量 100', 'up'); } + e.preventDefault(); } // 下箭头 = 音量- @@ -642,14 +643,21 @@ function PlayPageClient() { if (player.volume > 0) { player.volume -= 0.1; displayShortcutHint(`音量 ${Math.round(player.volume * 100)}`, 'down'); - e.preventDefault(); + } else { + displayShortcutHint('音量 0', 'down'); } + e.preventDefault(); } // 空格 = 播放/暂停 if (e.key === ' ') { - if (player.paused) player.play(); - else player.pause(); + if (playerRef.current.paused) { + playerRef.current.play(); + displayShortcutHint('播放', 'play'); + } else { + playerRef.current.pause(); + displayShortcutHint('暂停', 'pause'); + } e.preventDefault(); } @@ -836,13 +844,23 @@ function PlayPageClient() { if (!player) return; return player.subscribe(({ fullscreen }: any) => { + setIsFullscreen(fullscreen); if (fullscreen) { lockLandscape(); } else { unlock(); + // 强制重绘逻辑,解决退出全屏的黑屏/白边问题 + const playerEl = playerRef.current?.el as HTMLElement | null; + if (playerEl) { + playerEl.style.display = 'none'; + setTimeout(() => { + playerEl.style.display = ''; + setVH(); + }, 0); + } } }); - }, [playerRef.current]); + }, [playerRef.current, setVH]); useEffect(() => { // 播放页挂载时,锁定页面滚动并消除 body 100vh 带来的额外空白 @@ -952,22 +970,16 @@ function PlayPageClient() { e.stopPropagation(); }; - // @ts-ignore playerEl.addEventListener('touchstart', handleTouchStart, { passive: true, }); - // @ts-ignore playerEl.addEventListener('touchend', handleTouchEnd, { passive: true }); - // @ts-ignore playerEl.addEventListener('touchcancel', handleTouchEnd, { passive: true }); playerEl.addEventListener('contextmenu', disableContextMenu); return () => { - // @ts-ignore playerEl.removeEventListener('touchstart', handleTouchStart); - // @ts-ignore playerEl.removeEventListener('touchend', handleTouchEnd); - // @ts-ignore playerEl.removeEventListener('touchcancel', handleTouchEnd); playerEl.removeEventListener('contextmenu', disableContextMenu); }; @@ -1027,6 +1039,7 @@ function PlayPageClient() { sourceName, onToggleFavorite, onOpenSourcePanel, + isFullscreen, }: { videoTitle: string; favorited: boolean; @@ -1035,6 +1048,7 @@ function PlayPageClient() { sourceName: string; onToggleFavorite: () => void; onOpenSourcePanel: () => void; + isFullscreen: boolean; }) => { return (
{/* 返回按钮 */} - + + + + + )} {/* 中央标题及集数信息 */} -
+
{videoTitle} @@ -1106,6 +1126,8 @@ function PlayPageClient() { return (
@@ -1187,6 +1209,7 @@ function PlayPageClient() { sourceName={detail?.videoInfo.source_name || ''} onToggleFavorite={handleToggleFavorite} onOpenSourcePanel={handleSourcePanelOpen} + isFullscreen={isFullscreen} /> 1 ? ( - // Desktop-only next button - - ) : null, + muteButton: null, // 隐藏静音按钮 + volumeSlider: null, // 隐藏音量条 beforeCurrentTime: totalEpisodes > 1 ? ( - // Mobile-only next button + // 下一集按钮放在时间显示前 @@ -1258,13 +1266,16 @@ function PlayPageClient() { {showEpisodePanel && (
setShowEpisodePanel(false)} + onClick={() => { + setShowEpisodePanel(false); + playerContainerRef.current?.focus(); + }} /> )} {/* 侧拉面板 */}
@@ -1272,7 +1283,10 @@ function PlayPageClient() {

选集列表

diff --git a/tailwind.config.ts b/tailwind.config.ts index 5c32d2f..55cb3fb 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -9,6 +9,11 @@ const config: Config = { ], theme: { extend: { + screens: { + 'mobile-landscape': { + raw: '(orientation: landscape) and (max-height: 700px)', + }, + }, fontFamily: { primary: ['Inter', ...defaultTheme.fontFamily.sans], },