diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index a3588da..f5d4ba7 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -2,7 +2,7 @@ 'use client'; -import { AlertCircle, Cloud, Heart, Loader2, Router, Sparkles, X } from 'lucide-react'; +import { AlertCircle, Cloud, Heart, Keyboard, Loader2, Router, Sparkles, X } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useEffect, useMemo, useRef, useState } from 'react'; @@ -128,6 +128,34 @@ interface CustomSubtitleState { episodeIndex: number; } +const PLAYBACK_RATE_OPTIONS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4]; +const PLAY_SHORTCUT_GROUPS = [ + { + title: '播放控制', + items: [ + { keys: ['空格'], description: '播放 / 暂停' }, + { keys: ['←', '→'], description: '快退 / 快进 10 秒' }, + { keys: ['↑', '↓'], description: '音量增加 / 减少' }, + { keys: ['F'], description: '切换全屏' }, + ], + }, + { + title: '剧集切换', + items: [ + { keys: ['Alt', '←'], description: '上一集' }, + { keys: ['Alt', '→'], description: '下一集' }, + ], + }, + { + title: '倍速控制', + items: [ + { keys: ['小键盘 +'], description: '提高一档倍速' }, + { keys: ['小键盘 -'], description: '降低一档倍速' }, + { keys: ['小键盘 /'], description: '恢复 1x' }, + ], + }, +]; + function PlayPageClient() { const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080'; const router = useRouter(); @@ -181,6 +209,26 @@ function PlayPageClient() { // 详情面板状态 const [showDetailPanel, setShowDetailPanel] = useState(false); + // 快捷键说明弹窗状态 + const [showShortcutDialog, setShowShortcutDialog] = useState(false); + + useEffect(() => { + if (!showShortcutDialog) { + return; + } + + const handleShortcutDialogKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setShowShortcutDialog(false); + } + }; + + document.addEventListener('keydown', handleShortcutDialogKeyDown); + return () => { + document.removeEventListener('keydown', handleShortcutDialogKeyDown); + }; + }, [showShortcutDialog]); + // 大屏设备检测(判断选集面板是否在右侧) const [isLargeScreen, setIsLargeScreen] = useState(false); @@ -1610,6 +1658,55 @@ function PlayPageClient() { localStorage.setItem('preferredPlaybackRate', String(rate)); }; + const adjustPlaybackRateByStep = (direction: 1 | -1) => { + if (!artPlayerRef.current) { + return false; + } + + const currentRate = artPlayerRef.current.playbackRate || 1; + const currentIndex = PLAYBACK_RATE_OPTIONS.reduce((nearestIndex, rate, index) => { + return Math.abs(rate - currentRate) < Math.abs(PLAYBACK_RATE_OPTIONS[nearestIndex] - currentRate) + ? index + : nearestIndex; + }, 0); + let nextIndex = -1; + if (direction > 0) { + nextIndex = PLAYBACK_RATE_OPTIONS.findIndex((rate) => rate > currentRate + 0.01); + } else { + for (let index = PLAYBACK_RATE_OPTIONS.length - 1; index >= 0; index--) { + if (PLAYBACK_RATE_OPTIONS[index] < currentRate - 0.01) { + nextIndex = index; + break; + } + } + } + const boundedNextIndex = nextIndex === -1 ? currentIndex : nextIndex; + const effectiveNextIndex = Math.min( + Math.max(boundedNextIndex, 0), + PLAYBACK_RATE_OPTIONS.length - 1 + ); + const nextRate = PLAYBACK_RATE_OPTIONS[effectiveNextIndex]; + + artPlayerRef.current.playbackRate = nextRate; + artPlayerRef.current.notice.show = + effectiveNextIndex === currentIndex + ? direction > 0 + ? `已是最高倍速:${nextRate}x` + : `已是最低倍速:${nextRate}x` + : `倍速:${nextRate}x`; + return true; + }; + + const resetPlaybackRate = () => { + if (!artPlayerRef.current) { + return false; + } + + artPlayerRef.current.playbackRate = 1; + artPlayerRef.current.notice.show = '倍速:1x'; + return true; + }; + const isDanmakuAutoLoadDisabled = () => { if (typeof window === 'undefined') { return false; @@ -5795,6 +5892,27 @@ function PlayPageClient() { } } + // 小键盘 + = 倍速+ + if (e.code === 'NumpadAdd') { + if (adjustPlaybackRateByStep(1)) { + e.preventDefault(); + } + } + + // 小键盘 - = 倍速- + if (e.code === 'NumpadSubtract') { + if (adjustPlaybackRateByStep(-1)) { + e.preventDefault(); + } + } + + // 小键盘 / = 恢复 1x + if (e.code === 'NumpadDivide') { + if (resetPlaybackRate()) { + e.preventDefault(); + } + } + // f 键 = 切换全屏 if (e.key === 'f' || e.key === 'F') { if (artPlayerRef.current) { @@ -6281,7 +6399,7 @@ function PlayPageClient() { const CustomHlsJsLoader = createCustomHlsLoader(Hls); // 创建新的播放器实例 - Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4]; + Artplayer.PLAYBACK_RATE = PLAYBACK_RATE_OPTIONS; Artplayer.USE_RAF = true; // 获取当前集的字幕 @@ -9437,6 +9555,22 @@ function PlayPageClient() { )} + {/* 快捷键说明 */} + { + e.preventDefault(); + setShowShortcutDialog(true); + }} + className='group relative flex items-center justify-center gap-1 w-8 h-8 lg:w-auto lg:h-auto lg:px-2 lg:py-1.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-500 dark:hover:bg-gray-400 text-xs font-medium rounded-md transition-all duration-200 shadow-sm hover:shadow-md cursor-pointer overflow-hidden border border-gray-300 dark:border-gray-500 flex-shrink-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-950' + title='快捷键说明' + aria-label='查看播放快捷键说明' + > + + + 快捷键 + + + {/* PotPlayer */} { @@ -10064,6 +10198,83 @@ function PlayPageClient() { }} /> + {/* 快捷键说明弹窗 */} + {showShortcutDialog && ( + setShowShortcutDialog(false)} + > + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role='dialog' + aria-modal='true' + aria-labelledby='shortcut-dialog-title' + > + + + + + + + + + 播放快捷键 + + + + setShowShortcutDialog(false)} + className='rounded-lg p-2 text-gray-500 transition-colors duration-200 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500 cursor-pointer dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white' + aria-label='关闭快捷键说明' + > + + + + + + + {PLAY_SHORTCUT_GROUPS.map((group) => ( + + + {group.title} + + + {group.items.map((item) => ( + + + {item.keys.map((key, index) => ( + + {index > 0 && ( + + + )} + + {key} + + + ))} + + + {item.description} + + + ))} + + + ))} + + + + + )} + {/* 网盘搜索弹窗 */} {showPansouDialog && ( isLargeScreen ? (