Skip to content

Commit 8f20646

Browse files
committed
增加倍数快捷键,增加快捷键说明
1 parent 99bcc8e commit 8f20646

1 file changed

Lines changed: 213 additions & 2 deletions

File tree

src/app/play/page.tsx

Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
'use client';
44

5-
import { AlertCircle, Cloud, Heart, Loader2, Router, Sparkles, X } from 'lucide-react';
5+
import { AlertCircle, Cloud, Heart, Keyboard, Loader2, Router, Sparkles, X } from 'lucide-react';
66
import { useRouter, useSearchParams } from 'next/navigation';
77
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
88

@@ -128,6 +128,34 @@ interface CustomSubtitleState {
128128
episodeIndex: number;
129129
}
130130

131+
const PLAYBACK_RATE_OPTIONS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
132+
const PLAY_SHORTCUT_GROUPS = [
133+
{
134+
title: '播放控制',
135+
items: [
136+
{ keys: ['空格'], description: '播放 / 暂停' },
137+
{ keys: ['←', '→'], description: '快退 / 快进 10 秒' },
138+
{ keys: ['↑', '↓'], description: '音量增加 / 减少' },
139+
{ keys: ['F'], description: '切换全屏' },
140+
],
141+
},
142+
{
143+
title: '剧集切换',
144+
items: [
145+
{ keys: ['Alt', '←'], description: '上一集' },
146+
{ keys: ['Alt', '→'], description: '下一集' },
147+
],
148+
},
149+
{
150+
title: '倍速控制',
151+
items: [
152+
{ keys: ['小键盘 +'], description: '提高一档倍速' },
153+
{ keys: ['小键盘 -'], description: '降低一档倍速' },
154+
{ keys: ['小键盘 /'], description: '恢复 1x' },
155+
],
156+
},
157+
];
158+
131159
function PlayPageClient() {
132160
const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080';
133161
const router = useRouter();
@@ -181,6 +209,26 @@ function PlayPageClient() {
181209
// 详情面板状态
182210
const [showDetailPanel, setShowDetailPanel] = useState(false);
183211

212+
// 快捷键说明弹窗状态
213+
const [showShortcutDialog, setShowShortcutDialog] = useState(false);
214+
215+
useEffect(() => {
216+
if (!showShortcutDialog) {
217+
return;
218+
}
219+
220+
const handleShortcutDialogKeyDown = (event: KeyboardEvent) => {
221+
if (event.key === 'Escape') {
222+
setShowShortcutDialog(false);
223+
}
224+
};
225+
226+
document.addEventListener('keydown', handleShortcutDialogKeyDown);
227+
return () => {
228+
document.removeEventListener('keydown', handleShortcutDialogKeyDown);
229+
};
230+
}, [showShortcutDialog]);
231+
184232
// 大屏设备检测(判断选集面板是否在右侧)
185233
const [isLargeScreen, setIsLargeScreen] = useState(false);
186234

@@ -1610,6 +1658,55 @@ function PlayPageClient() {
16101658
localStorage.setItem('preferredPlaybackRate', String(rate));
16111659
};
16121660

1661+
const adjustPlaybackRateByStep = (direction: 1 | -1) => {
1662+
if (!artPlayerRef.current) {
1663+
return false;
1664+
}
1665+
1666+
const currentRate = artPlayerRef.current.playbackRate || 1;
1667+
const currentIndex = PLAYBACK_RATE_OPTIONS.reduce((nearestIndex, rate, index) => {
1668+
return Math.abs(rate - currentRate) < Math.abs(PLAYBACK_RATE_OPTIONS[nearestIndex] - currentRate)
1669+
? index
1670+
: nearestIndex;
1671+
}, 0);
1672+
let nextIndex = -1;
1673+
if (direction > 0) {
1674+
nextIndex = PLAYBACK_RATE_OPTIONS.findIndex((rate) => rate > currentRate + 0.01);
1675+
} else {
1676+
for (let index = PLAYBACK_RATE_OPTIONS.length - 1; index >= 0; index--) {
1677+
if (PLAYBACK_RATE_OPTIONS[index] < currentRate - 0.01) {
1678+
nextIndex = index;
1679+
break;
1680+
}
1681+
}
1682+
}
1683+
const boundedNextIndex = nextIndex === -1 ? currentIndex : nextIndex;
1684+
const effectiveNextIndex = Math.min(
1685+
Math.max(boundedNextIndex, 0),
1686+
PLAYBACK_RATE_OPTIONS.length - 1
1687+
);
1688+
const nextRate = PLAYBACK_RATE_OPTIONS[effectiveNextIndex];
1689+
1690+
artPlayerRef.current.playbackRate = nextRate;
1691+
artPlayerRef.current.notice.show =
1692+
effectiveNextIndex === currentIndex
1693+
? direction > 0
1694+
? `已是最高倍速:${nextRate}x`
1695+
: `已是最低倍速:${nextRate}x`
1696+
: `倍速:${nextRate}x`;
1697+
return true;
1698+
};
1699+
1700+
const resetPlaybackRate = () => {
1701+
if (!artPlayerRef.current) {
1702+
return false;
1703+
}
1704+
1705+
artPlayerRef.current.playbackRate = 1;
1706+
artPlayerRef.current.notice.show = '倍速:1x';
1707+
return true;
1708+
};
1709+
16131710
const isDanmakuAutoLoadDisabled = () => {
16141711
if (typeof window === 'undefined') {
16151712
return false;
@@ -5795,6 +5892,27 @@ function PlayPageClient() {
57955892
}
57965893
}
57975894

5895+
// 小键盘 + = 倍速+
5896+
if (e.code === 'NumpadAdd') {
5897+
if (adjustPlaybackRateByStep(1)) {
5898+
e.preventDefault();
5899+
}
5900+
}
5901+
5902+
// 小键盘 - = 倍速-
5903+
if (e.code === 'NumpadSubtract') {
5904+
if (adjustPlaybackRateByStep(-1)) {
5905+
e.preventDefault();
5906+
}
5907+
}
5908+
5909+
// 小键盘 / = 恢复 1x
5910+
if (e.code === 'NumpadDivide') {
5911+
if (resetPlaybackRate()) {
5912+
e.preventDefault();
5913+
}
5914+
}
5915+
57985916
// f 键 = 切换全屏
57995917
if (e.key === 'f' || e.key === 'F') {
58005918
if (artPlayerRef.current) {
@@ -6281,7 +6399,7 @@ function PlayPageClient() {
62816399
const CustomHlsJsLoader = createCustomHlsLoader(Hls);
62826400

62836401
// 创建新的播放器实例
6284-
Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
6402+
Artplayer.PLAYBACK_RATE = PLAYBACK_RATE_OPTIONS;
62856403
Artplayer.USE_RAF = true;
62866404

62876405
// 获取当前集的字幕
@@ -9437,6 +9555,22 @@ function PlayPageClient() {
94379555
</button>
94389556
)}
94399557

9558+
{/* 快捷键说明 */}
9559+
<button
9560+
onClick={(e) => {
9561+
e.preventDefault();
9562+
setShowShortcutDialog(true);
9563+
}}
9564+
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'
9565+
title='快捷键说明'
9566+
aria-label='查看播放快捷键说明'
9567+
>
9568+
<Keyboard className='w-4 h-4 flex-shrink-0 text-gray-700 dark:text-gray-200' />
9569+
<span className='hidden lg:inline max-w-0 group-hover:max-w-[100px] overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out text-gray-700 dark:text-gray-200'>
9570+
快捷键
9571+
</span>
9572+
</button>
9573+
94409574
{/* PotPlayer */}
94419575
<button
94429576
onClick={(e) => {
@@ -10064,6 +10198,83 @@ function PlayPageClient() {
1006410198
}}
1006510199
/>
1006610200

10201+
{/* 快捷键说明弹窗 */}
10202+
{showShortcutDialog && (
10203+
<div
10204+
className='fixed inset-0 z-[10000] flex items-center justify-center bg-black/50 px-4 py-6 backdrop-blur-sm'
10205+
onClick={() => setShowShortcutDialog(false)}
10206+
>
10207+
<div
10208+
className='relative w-full max-w-lg overflow-hidden rounded-2xl border border-gray-200 bg-white text-gray-900 shadow-2xl shadow-black/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:shadow-black/40'
10209+
onClick={(e) => e.stopPropagation()}
10210+
onKeyDown={(e) => e.stopPropagation()}
10211+
role='dialog'
10212+
aria-modal='true'
10213+
aria-labelledby='shortcut-dialog-title'
10214+
>
10215+
<div className='absolute inset-x-0 top-0 h-24 bg-gradient-to-br from-green-500/15 via-cyan-500/10 to-transparent pointer-events-none' />
10216+
<div className='relative flex items-start justify-between gap-4 border-b border-gray-200 px-5 py-4 dark:border-gray-700'>
10217+
<div className='flex items-center gap-3'>
10218+
<div className='flex h-10 w-10 items-center justify-center rounded-xl border border-green-500/30 bg-green-500/10 text-green-600 dark:text-green-300'>
10219+
<Keyboard className='h-5 w-5' />
10220+
</div>
10221+
<div>
10222+
<h2 id='shortcut-dialog-title' className='text-base font-semibold text-gray-950 dark:text-white'>
10223+
播放快捷键
10224+
</h2>
10225+
</div>
10226+
</div>
10227+
<button
10228+
onClick={() => setShowShortcutDialog(false)}
10229+
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'
10230+
aria-label='关闭快捷键说明'
10231+
>
10232+
<X className='h-5 w-5' />
10233+
</button>
10234+
</div>
10235+
10236+
<div className='relative max-h-[70vh] overflow-y-auto px-5 py-4'>
10237+
<div className='grid gap-3'>
10238+
{PLAY_SHORTCUT_GROUPS.map((group) => (
10239+
<section
10240+
key={group.title}
10241+
className='rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800/60'
10242+
>
10243+
<h3 className='mb-3 text-sm font-medium text-gray-800 dark:text-gray-200'>
10244+
{group.title}
10245+
</h3>
10246+
<div className='space-y-2'>
10247+
{group.items.map((item) => (
10248+
<div
10249+
key={`${group.title}-${item.description}`}
10250+
className='flex items-center justify-between gap-4 rounded-lg px-2 py-1.5 transition-colors duration-200 hover:bg-white dark:hover:bg-gray-700/70'
10251+
>
10252+
<div className='flex flex-wrap items-center gap-1.5'>
10253+
{item.keys.map((key, index) => (
10254+
<span key={`${item.description}-${key}`} className='flex items-center gap-1.5'>
10255+
{index > 0 && (
10256+
<span className='text-xs text-gray-400 dark:text-gray-500'>+</span>
10257+
)}
10258+
<kbd className='min-w-7 rounded-md border border-gray-300 bg-white px-2 py-1 text-center text-xs font-semibold text-gray-800 shadow-sm dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 dark:shadow-inner dark:shadow-white/5'>
10259+
{key}
10260+
</kbd>
10261+
</span>
10262+
))}
10263+
</div>
10264+
<span className='text-right text-xs text-gray-600 dark:text-gray-300'>
10265+
{item.description}
10266+
</span>
10267+
</div>
10268+
))}
10269+
</div>
10270+
</section>
10271+
))}
10272+
</div>
10273+
</div>
10274+
</div>
10275+
</div>
10276+
)}
10277+
1006710278
{/* 网盘搜索弹窗 */}
1006810279
{showPansouDialog && (
1006910280
isLargeScreen ? (

0 commit comments

Comments
 (0)