From 8f20646e0b21db69fc95d1ae384087801a42dcb3 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 11 Jun 2026 20:05:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=80=8D=E6=95=B0=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E9=94=AE=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E9=94=AE=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/play/page.tsx | 215 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 2 deletions(-) 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() { )} + {/* 快捷键说明 */} + + {/* PotPlayer */} + + +
+
+ {PLAY_SHORTCUT_GROUPS.map((group) => ( +
+

+ {group.title} +

+
+ {group.items.map((item) => ( +
+
+ {item.keys.map((key, index) => ( + + {index > 0 && ( + + + )} + + {key} + + + ))} +
+ + {item.description} + +
+ ))} +
+
+ ))} +
+
+ + + )} + {/* 网盘搜索弹窗 */} {showPansouDialog && ( isLargeScreen ? (