diff --git a/src/app/tv/me/page.tsx b/src/app/tv/me/page.tsx index d45f5c1..68d9caf 100644 --- a/src/app/tv/me/page.tsx +++ b/src/app/tv/me/page.tsx @@ -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(null); const [loggingOut, setLoggingOut] = useState(false); const [error, setError] = useState(''); + const [upDownAction, setUpDownAction] = useState( + DEFAULT_TV_PLAYER_UP_DOWN_ACTION + ); + const wakeMenuButtonRef = useRef(null); + const volumeButtonRef = useRef(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) => { + 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 (

正在读取登录信息

-

请稍候,电视端会自动跳转。

+

+ 请稍候,电视端会自动跳转。 +

); @@ -135,7 +181,9 @@ export default function TVMePage() { 账号角色 -
{roleText}
+
+ {roleText} +
@@ -162,7 +210,10 @@ export default function TVMePage() {
{error && ( -

+

{error}

)} @@ -182,6 +233,69 @@ export default function TVMePage() {
+ +
+
+
+
+ + 偏好设置 +
+

+ 这些设置保存在当前电视设备上。 +

+
+ +
+
+
+

+ 播放页上下键功能 +

+

+ 播放页遥控器 ↑ / ↓ 键执行的操作。 +

+
+
+ {upDownAction === 'wake-menu' ? '唤醒菜单' : '音量控制'} +
+
+ +
+ + +
+
+
+
diff --git a/src/app/tv/play/page.tsx b/src/app/tv/play/page.tsx index 3906d8d..db1ef83 100644 --- a/src/app/tv/play/page.tsx +++ b/src/app/tv/play/page.tsx @@ -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, ]); diff --git a/src/lib/tv-preferences.ts b/src/lib/tv-preferences.ts new file mode 100644 index 0000000..c129a5c --- /dev/null +++ b/src/lib/tv-preferences.ts @@ -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 + } +}