增加倍数快捷键,增加快捷键说明

This commit is contained in:
mtvpls
2026-06-11 20:05:51 +08:00
parent 99bcc8ed77
commit 8f20646e0b
+213 -2
View File
@@ -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() {
</button>
)}
{/* 快捷键说明 */}
<button
onClick={(e) => {
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='查看播放快捷键说明'
>
<Keyboard className='w-4 h-4 flex-shrink-0 text-gray-700 dark:text-gray-200' />
<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'>
</span>
</button>
{/* PotPlayer */}
<button
onClick={(e) => {
@@ -10064,6 +10198,83 @@ function PlayPageClient() {
}}
/>
{/* 快捷键说明弹窗 */}
{showShortcutDialog && (
<div
className='fixed inset-0 z-[10000] flex items-center justify-center bg-black/50 px-4 py-6 backdrop-blur-sm'
onClick={() => setShowShortcutDialog(false)}
>
<div
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'
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
role='dialog'
aria-modal='true'
aria-labelledby='shortcut-dialog-title'
>
<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' />
<div className='relative flex items-start justify-between gap-4 border-b border-gray-200 px-5 py-4 dark:border-gray-700'>
<div className='flex items-center gap-3'>
<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'>
<Keyboard className='h-5 w-5' />
</div>
<div>
<h2 id='shortcut-dialog-title' className='text-base font-semibold text-gray-950 dark:text-white'>
</h2>
</div>
</div>
<button
onClick={() => 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='关闭快捷键说明'
>
<X className='h-5 w-5' />
</button>
</div>
<div className='relative max-h-[70vh] overflow-y-auto px-5 py-4'>
<div className='grid gap-3'>
{PLAY_SHORTCUT_GROUPS.map((group) => (
<section
key={group.title}
className='rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800/60'
>
<h3 className='mb-3 text-sm font-medium text-gray-800 dark:text-gray-200'>
{group.title}
</h3>
<div className='space-y-2'>
{group.items.map((item) => (
<div
key={`${group.title}-${item.description}`}
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'
>
<div className='flex flex-wrap items-center gap-1.5'>
{item.keys.map((key, index) => (
<span key={`${item.description}-${key}`} className='flex items-center gap-1.5'>
{index > 0 && (
<span className='text-xs text-gray-400 dark:text-gray-500'>+</span>
)}
<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'>
{key}
</kbd>
</span>
))}
</div>
<span className='text-right text-xs text-gray-600 dark:text-gray-300'>
{item.description}
</span>
</div>
))}
</div>
</section>
))}
</div>
</div>
</div>
</div>
)}
{/* 网盘搜索弹窗 */}
{showPansouDialog && (
isLargeScreen ? (