web tv模式增加上下键可改为菜单模式
This commit is contained in:
+118
-4
@@ -5,13 +5,28 @@ import {
|
||||
Clock3,
|
||||
Loader2,
|
||||
LogOut,
|
||||
Menu,
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
User,
|
||||
Volume2,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { clearAuthCookie, getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||
import {
|
||||
type TVPlayerUpDownAction,
|
||||
DEFAULT_TV_PLAYER_UP_DOWN_ACTION,
|
||||
loadTVPlayerUpDownAction,
|
||||
saveTVPlayerUpDownAction,
|
||||
} from '@/lib/tv-preferences';
|
||||
|
||||
import TVLayout from '@/components/tv/TVLayout';
|
||||
|
||||
@@ -56,10 +71,16 @@ export default function TVMePage() {
|
||||
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
|
||||
const [loggingOut, setLoggingOut] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [upDownAction, setUpDownAction] = useState<TVPlayerUpDownAction>(
|
||||
DEFAULT_TV_PLAYER_UP_DOWN_ACTION
|
||||
);
|
||||
const wakeMenuButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const volumeButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const auth = getAuthInfoFromBrowserCookie();
|
||||
setAuthInfo(auth);
|
||||
setUpDownAction(loadTVPlayerUpDownAction());
|
||||
setReady(true);
|
||||
}, []);
|
||||
|
||||
@@ -92,13 +113,38 @@ export default function TVMePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpDownActionChange = (action: TVPlayerUpDownAction) => {
|
||||
setUpDownAction(action);
|
||||
saveTVPlayerUpDownAction(action);
|
||||
};
|
||||
|
||||
const handleUpDownActionKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const nextAction: TVPlayerUpDownAction =
|
||||
event.key === 'ArrowRight' ? 'volume' : 'wake-menu';
|
||||
handleUpDownActionChange(nextAction);
|
||||
window.requestAnimationFrame(() => {
|
||||
const target =
|
||||
nextAction === 'wake-menu'
|
||||
? wakeMenuButtonRef.current
|
||||
: volumeButtonRef.current;
|
||||
target?.focus({ preventScroll: true });
|
||||
});
|
||||
};
|
||||
|
||||
if (!ready || !authInfo) {
|
||||
return (
|
||||
<TVLayout>
|
||||
<section className='mx-auto max-w-5xl rounded-[42px] border border-white/10 bg-slate-950/75 p-12 text-center shadow-2xl shadow-black/60'>
|
||||
<Loader2 className='mx-auto h-16 w-16 animate-spin text-rose-400' />
|
||||
<h1 className='mt-6 text-5xl font-black'>正在读取登录信息</h1>
|
||||
<p className='mt-4 text-2xl text-slate-300'>请稍候,电视端会自动跳转。</p>
|
||||
<p className='mt-4 text-2xl text-slate-300'>
|
||||
请稍候,电视端会自动跳转。
|
||||
</p>
|
||||
</section>
|
||||
</TVLayout>
|
||||
);
|
||||
@@ -135,7 +181,9 @@ export default function TVMePage() {
|
||||
<ShieldCheck className='h-6 w-6 text-rose-300' />
|
||||
账号角色
|
||||
</div>
|
||||
<div className='mt-4 text-4xl font-black text-white'>{roleText}</div>
|
||||
<div className='mt-4 text-4xl font-black text-white'>
|
||||
{roleText}
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-[28px] border border-white/10 bg-white/[0.06] p-6'>
|
||||
<div className='flex items-center gap-3 text-xl font-bold text-slate-300'>
|
||||
@@ -162,7 +210,10 @@ export default function TVMePage() {
|
||||
|
||||
<div className='mt-10'>
|
||||
{error && (
|
||||
<p role='alert' className='mb-4 rounded-2xl border border-red-400/40 bg-red-950/50 p-4 text-xl text-red-100'>
|
||||
<p
|
||||
role='alert'
|
||||
className='mb-4 rounded-2xl border border-red-400/40 bg-red-950/50 p-4 text-xl text-red-100'
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
@@ -182,6 +233,69 @@ export default function TVMePage() {
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div className='relative mt-10 rounded-[34px] border border-white/10 bg-black/35 p-7'>
|
||||
<div className='flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between'>
|
||||
<div className='max-w-2xl'>
|
||||
<div className='flex items-center gap-3 text-3xl font-black text-white'>
|
||||
<SlidersHorizontal className='h-9 w-9 text-rose-300' />
|
||||
偏好设置
|
||||
</div>
|
||||
<p className='mt-3 text-xl leading-relaxed text-slate-300'>
|
||||
这些设置保存在当前电视设备上。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='w-full rounded-[28px] border border-white/10 bg-white/[0.06] p-6 lg:max-w-[560px]'>
|
||||
<div className='flex items-start justify-between gap-5'>
|
||||
<div>
|
||||
<h3 className='text-2xl font-black text-white'>
|
||||
播放页上下键功能
|
||||
</h3>
|
||||
<p className='mt-2 text-lg leading-relaxed text-slate-300'>
|
||||
播放页遥控器 ↑ / ↓ 键执行的操作。
|
||||
</p>
|
||||
</div>
|
||||
<div className='shrink-0 rounded-2xl bg-slate-950/55 px-4 py-2 text-lg font-black text-rose-100'>
|
||||
{upDownAction === 'wake-menu' ? '唤醒菜单' : '音量控制'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tv-preference-up-down-action
|
||||
onKeyDownCapture={handleUpDownActionKeyDown}
|
||||
className='mt-5 grid gap-3 sm:grid-cols-2'
|
||||
>
|
||||
<button
|
||||
ref={wakeMenuButtonRef}
|
||||
type='button'
|
||||
onClick={() => handleUpDownActionChange('wake-menu')}
|
||||
className={`tv-focusable flex cursor-pointer items-center justify-center gap-3 rounded-2xl px-5 py-4 text-xl font-black outline-none transition focus:ring-4 focus:ring-rose-300 ${
|
||||
upDownAction === 'wake-menu'
|
||||
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
|
||||
: 'bg-white/10 text-slate-200 hover:bg-white/15'
|
||||
}`}
|
||||
>
|
||||
<Menu className='h-6 w-6' />
|
||||
唤醒菜单
|
||||
</button>
|
||||
<button
|
||||
ref={volumeButtonRef}
|
||||
type='button'
|
||||
onClick={() => handleUpDownActionChange('volume')}
|
||||
className={`tv-focusable flex cursor-pointer items-center justify-center gap-3 rounded-2xl px-5 py-4 text-xl font-black outline-none transition focus:ring-4 focus:ring-rose-300 ${
|
||||
upDownAction === 'volume'
|
||||
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
|
||||
: 'bg-white/10 text-slate-200 hover:bg-white/15'
|
||||
}`}
|
||||
>
|
||||
<Volume2 className='h-6 w-6' />
|
||||
音量控制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</TVLayout>
|
||||
|
||||
+21
-14
@@ -17,16 +17,16 @@ import {
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
SlidersHorizontal,
|
||||
X,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
Suspense,
|
||||
type Dispatch,
|
||||
type FocusEvent,
|
||||
type SetStateAction,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -34,16 +34,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
deleteFavorite,
|
||||
generateStorageKey,
|
||||
getAllPlayRecords,
|
||||
getSkipConfig,
|
||||
isFavorited,
|
||||
saveFavorite,
|
||||
savePlayRecord,
|
||||
} from '@/lib/db.client';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
import {
|
||||
convertDanmakuFormat,
|
||||
getDanmakuById,
|
||||
@@ -53,6 +43,17 @@ import {
|
||||
saveDanmakuDisplayState,
|
||||
searchAnime,
|
||||
} from '@/lib/danmaku/api';
|
||||
import {
|
||||
deleteFavorite,
|
||||
generateStorageKey,
|
||||
getAllPlayRecords,
|
||||
getSkipConfig,
|
||||
isFavorited,
|
||||
saveFavorite,
|
||||
savePlayRecord,
|
||||
} from '@/lib/db.client';
|
||||
import { loadTVPlayerUpDownAction } from '@/lib/tv-preferences';
|
||||
import { SearchResult } from '@/lib/types';
|
||||
|
||||
import TVNativeVideo from '@/components/tv/player/TVNativeVideo';
|
||||
import {
|
||||
@@ -241,6 +242,7 @@ function TVPlayClient() {
|
||||
const [muted, setMuted] = useState(initialVolumeState.muted);
|
||||
const [volume, setVolume] = useState(initialVolumeState.volume);
|
||||
const [showVolumeHint, setShowVolumeHint] = useState(false);
|
||||
const [upDownAction] = useState(loadTVPlayerUpDownAction);
|
||||
const [seekHint, setSeekHint] = useState<{
|
||||
current: number;
|
||||
duration: number;
|
||||
@@ -324,7 +326,7 @@ function TVPlayClient() {
|
||||
setSources(data.sources);
|
||||
const maxIndex = Math.max(0, (data.detail.episodes?.length || 1) - 1);
|
||||
const explicitIndex = searchParams.has('index');
|
||||
let safeIndex = Math.max(
|
||||
const safeIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
initialIndex || data.detail.initialEpisodeIndex || 0,
|
||||
@@ -922,7 +924,11 @@ function TVPlayClient() {
|
||||
(event.key === 'ArrowUp' || event.key === 'ArrowDown')
|
||||
) {
|
||||
event.preventDefault();
|
||||
setVideoVolume(volume + (event.key === 'ArrowUp' ? 0.05 : -0.05));
|
||||
if (upDownAction === 'volume') {
|
||||
setVideoVolume(volume + (event.key === 'ArrowUp' ? 0.05 : -0.05));
|
||||
} else {
|
||||
revealPanel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -958,6 +964,7 @@ function TVPlayClient() {
|
||||
showDetail,
|
||||
showEpisodes,
|
||||
showPanel,
|
||||
upDownAction,
|
||||
volume,
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export type TVPlayerUpDownAction = 'wake-menu' | 'volume';
|
||||
|
||||
export const TV_PLAYER_UP_DOWN_ACTION_KEY = 'tv_player_up_down_action';
|
||||
export const DEFAULT_TV_PLAYER_UP_DOWN_ACTION: TVPlayerUpDownAction =
|
||||
'wake-menu';
|
||||
|
||||
export function normalizeTVPlayerUpDownAction(
|
||||
value: unknown
|
||||
): TVPlayerUpDownAction {
|
||||
return value === 'volume' ? 'volume' : DEFAULT_TV_PLAYER_UP_DOWN_ACTION;
|
||||
}
|
||||
|
||||
export function loadTVPlayerUpDownAction(): TVPlayerUpDownAction {
|
||||
if (typeof window === 'undefined') return DEFAULT_TV_PLAYER_UP_DOWN_ACTION;
|
||||
|
||||
try {
|
||||
return normalizeTVPlayerUpDownAction(
|
||||
localStorage.getItem(TV_PLAYER_UP_DOWN_ACTION_KEY)
|
||||
);
|
||||
} catch {
|
||||
return DEFAULT_TV_PLAYER_UP_DOWN_ACTION;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveTVPlayerUpDownAction(action: TVPlayerUpDownAction): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(TV_PLAYER_UP_DOWN_ACTION_KEY, action);
|
||||
} catch {
|
||||
// ignore localStorage failures in private/limited modes
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user