增加倍数快捷键,增加快捷键说明
This commit is contained in:
+213
-2
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user