feat: change manifest theme color

This commit is contained in:
shinya
2025-06-25 22:33:44 +08:00
parent dbbed5e38f
commit c14cd4022b
2 changed files with 425 additions and 417 deletions

View File

@@ -21,7 +21,7 @@ export default function RootLayout({
return ( return (
<html lang='zh-CN'> <html lang='zh-CN'>
<head> <head>
<meta name='theme-color' content='#ffffffb3' /> <meta name='theme-color' content='#e6f3fb' />
</head> </head>
<body className={`${inter.className} min-h-screen text-gray-900`}> <body className={`${inter.className} min-h-screen text-gray-900`}>
<AuthProvider>{children}</AuthProvider> <AuthProvider>{children}</AuthProvider>

View File

@@ -16,7 +16,6 @@ import {
} from '@vidstack/react/player/layouts/default'; } from '@vidstack/react/player/layouts/default';
import Hls from 'hls.js'; import Hls from 'hls.js';
import { Heart } from 'lucide-react'; import { Heart } from 'lucide-react';
import Head from 'next/head';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
@@ -994,6 +993,30 @@ function PlayPageClient() {
}; };
}, [playerRef.current, isLongPressing]); }, [playerRef.current, isLongPressing]);
/* -------------------- 设置 meta theme-color 为纯黑 -------------------- */
useEffect(() => {
if (typeof document === 'undefined') return;
// 查找或创建 meta[name="theme-color"]
let metaTag = document.querySelector(
'meta[name="theme-color"]'
) as HTMLMetaElement | null;
if (!metaTag) {
metaTag = document.createElement('meta');
metaTag.setAttribute('name', 'theme-color');
document.head.appendChild(metaTag);
}
// 记录原始颜色,并设置为纯黑
metaTag.setAttribute('content', '#000000');
// 卸载时恢复
return () => {
metaTag?.setAttribute('content', '#e6f3fb');
};
}, []);
if (loading) { if (loading) {
return ( return (
<div className='min-h-[100dvh] bg-black flex items-center justify-center overflow-hidden overscroll-contain'> <div className='min-h-[100dvh] bg-black flex items-center justify-center overflow-hidden overscroll-contain'>
@@ -1148,232 +1171,154 @@ function PlayPageClient() {
}; };
return ( return (
<> <div
<Head> ref={playerContainerRef}
<meta name='theme-color' content='#000000' /> tabIndex={-1}
</Head> className='bg-black fixed inset-0 overflow-hidden overscroll-contain'
<div style={{ height: 'calc(var(--vh, 1vh) * 100)' }}
ref={playerContainerRef} >
tabIndex={-1} {/* 竖屏提示蒙层 */}
className='bg-black fixed inset-0 overflow-hidden overscroll-contain' {showOrientationTip && (
style={{ height: 'calc(var(--vh, 1vh) * 100)' }} <div className='fixed bottom-16 left-1/2 -translate-x-1/2 z-[190] flex items-center px-4 py-2 rounded bg-black/70 text-white space-x-2 pointer-events-none backdrop-blur-sm'>
> <svg
{/* 竖屏提示蒙层 */} className='w-5 h-5'
{showOrientationTip && ( fill='none'
<div className='fixed bottom-16 left-1/2 -translate-x-1/2 z-[190] flex items-center px-4 py-2 rounded bg-black/70 text-white space-x-2 pointer-events-none backdrop-blur-sm'> stroke='currentColor'
<svg viewBox='0 0 24 24'
className='w-5 h-5'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M12 4v16m8-8H4'
/>
</svg>
<span className='text-sm'></span>
</div>
)}
{/* 强制横屏按钮:仅在移动端竖屏时显示 */}
{isPortrait && (
<button
onClick={handleForceLandscape}
className='fixed bottom-16 left-4 z-[85] w-10 h-10 rounded-full bg-gray-800 text-white flex items-center justify-center md:hidden'
> >
<svg <path
className='w-6 h-6' strokeLinecap='round'
fill='none' strokeLinejoin='round'
stroke='currentColor' strokeWidth={2}
viewBox='0 0 24 24' d='M12 4v16m8-8H4'
> />
<path </svg>
strokeLinecap='round' <span className='text-sm'></span>
strokeLinejoin='round' </div>
strokeWidth={2} )}
d='M3 18v-6a3 3 0 013-3h12M21 6v6a3 3 0 01-3 3H6'
/>
</svg>
</button>
)}
{/* 换源加载遮罩 */} {/* 强制横屏按钮:仅在移动端竖屏时显示 */}
{sourceChanging && ( {isPortrait && (
<div className='fixed inset-0 bg-black/50 z-[200] flex items-center justify-center'> <button
<div className='text-white text-center'> onClick={handleForceLandscape}
<div className='animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500 mx-auto mb-2'></div> className='fixed bottom-16 left-4 z-[85] w-10 h-10 rounded-full bg-gray-800 text-white flex items-center justify-center md:hidden'
<div className='text-sm'>...</div>
</div>
</div>
)}
{/* 播放器容器 */}
<MediaPlayer
ref={playerRef}
className='w-full h-full group'
src={videoUrl}
poster={videoCover}
playsInline
autoPlay
crossOrigin='anonymous'
controlsDelay={3000}
keyDisabled
onCanPlay={onCanPlay}
onEnded={onEnded}
onTimeUpdate={onTimeUpdate}
onPause={saveCurrentPlayProgress}
onError={handlePlayerError}
onProviderChange={onProviderChange}
> >
<MediaProvider /> <svg
<PlayerUITopbar className='w-6 h-6'
videoTitle={videoTitle} fill='none'
favorited={favorited} stroke='currentColor'
totalEpisodes={totalEpisodes} viewBox='0 0 24 24'
currentEpisodeIndex={currentEpisodeIndex} >
sourceName={detail?.videoInfo.source_name || ''} <path
onToggleFavorite={handleToggleFavorite} strokeLinecap='round'
onOpenSourcePanel={handleSourcePanelOpen} strokeLinejoin='round'
isFullscreen={isFullscreen} strokeWidth={2}
/> d='M3 18v-6a3 3 0 013-3h12M21 6v6a3 3 0 01-3 3H6'
<DefaultVideoLayout />
icons={defaultLayoutIcons} </svg>
slots={{ </button>
googleCastButton: null, )}
pipButton: null,
settingsMenu: null, {/* 换源加载遮罩 */}
muteButton: null, // 隐藏静音按钮 {sourceChanging && (
volumeSlider: null, // 隐藏音量条 <div className='fixed inset-0 bg-black/50 z-[200] flex items-center justify-center'>
airPlayButton: null, // 隐藏默认 AirPlay 按钮 <div className='text-white text-center'>
beforeCurrentTime: <div className='animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500 mx-auto mb-2'></div>
totalEpisodes > 1 ? ( <div className='text-sm'>...</div>
// 下一集按钮放在时间显示前 </div>
</div>
)}
{/* 播放器容器 */}
<MediaPlayer
ref={playerRef}
className='w-full h-full group'
src={videoUrl}
poster={videoCover}
playsInline
autoPlay
crossOrigin='anonymous'
controlsDelay={3000}
keyDisabled
onCanPlay={onCanPlay}
onEnded={onEnded}
onTimeUpdate={onTimeUpdate}
onPause={saveCurrentPlayProgress}
onError={handlePlayerError}
onProviderChange={onProviderChange}
>
<MediaProvider />
<PlayerUITopbar
videoTitle={videoTitle}
favorited={favorited}
totalEpisodes={totalEpisodes}
currentEpisodeIndex={currentEpisodeIndex}
sourceName={detail?.videoInfo.source_name || ''}
onToggleFavorite={handleToggleFavorite}
onOpenSourcePanel={handleSourcePanelOpen}
isFullscreen={isFullscreen}
/>
<DefaultVideoLayout
icons={defaultLayoutIcons}
slots={{
googleCastButton: null,
pipButton: null,
settingsMenu: null,
muteButton: null, // 隐藏静音按钮
volumeSlider: null, // 隐藏音量条
airPlayButton: null, // 隐藏默认 AirPlay 按钮
beforeCurrentTime:
totalEpisodes > 1 ? (
// 下一集按钮放在时间显示前
<button
className='vds-button mr-2'
onClick={handleNextEpisode}
aria-label='Next Episode'
>
<svg
className='vds-icon'
viewBox='0 0 32 32'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M6 24l12-8L6 8v16zM22 8v16h3V8h-3z'
fill='currentColor'
/>
</svg>
</button>
) : null,
beforeFullscreenButton: (
<>
{totalEpisodes > 1 && (
<button <button
className='vds-button mr-2' className='vds-button mr-2'
onClick={handleNextEpisode} onClick={() => {
aria-label='Next Episode' setShowEpisodePanel(true);
playerContainerRef.current?.focus();
}}
> >
<svg
className='vds-icon'
viewBox='0 0 32 32'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M6 24l12-8L6 8v16zM22 8v16h3V8h-3z'
fill='currentColor'
/>
</svg>
</button> </button>
) : null, )}
beforeFullscreenButton: ( <PlaybackRateButton playerRef={playerRef} />
<> {/* 自定义 AirPlay 按钮 */}
{totalEpisodes > 1 && ( <AirPlayButton className='vds-button'>
<button <AirPlayIcon className='vds-icon' />
className='vds-button mr-2' </AirPlayButton>
onClick={() => { </>
setShowEpisodePanel(true); ),
playerContainerRef.current?.focus(); }}
}} />
>
</button>
)}
<PlaybackRateButton playerRef={playerRef} />
{/* 自定义 AirPlay 按钮 */}
<AirPlayButton className='vds-button'>
<AirPlayIcon className='vds-icon' />
</AirPlayButton>
</>
),
}}
/>
{/* 选集侧拉面板 */} {/* 选集侧拉面板 */}
{totalEpisodes > 1 && ( {totalEpisodes > 1 && (
<div data-episode-panel> <div data-episode-panel>
{/* 遮罩层 */}
{showEpisodePanel && (
<div
className='fixed inset-0 bg-black/50 z-[110]'
onClick={() => {
setShowEpisodePanel(false);
playerContainerRef.current?.focus();
}}
/>
)}
{/* 侧拉面板 */}
<div
className={`fixed top-0 right-0 h-full w-full mobile-landscape:w-1/2 md:w-80 bg-black/40 backdrop-blur-xl z-[110] transform transition-transform duration-300 ${
showEpisodePanel ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className='p-6 h-full flex flex-col'>
<div className='flex items-center justify-between mb-6'>
<h3 className='text-white text-xl font-semibold'>
</h3>
<button
onClick={() => {
setShowEpisodePanel(false);
playerContainerRef.current?.focus();
}}
className='text-gray-400 hover:text-white transition-colors'
>
<svg
className='w-6 h-6'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
<div className='text-gray-300 text-sm mb-4'>
当前: {currentEpisodeIndex + 1} / {totalEpisodes}{' '}
</div>
<div className='flex-1 overflow-y-auto'>
<div className='grid grid-cols-4 gap-3'>
{Array.from({ length: totalEpisodes }, (_, idx) => (
<button
key={idx}
onClick={() => handleEpisodeChange(idx)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
idx === currentEpisodeIndex
? 'bg-green-500 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{idx + 1}
</button>
))}
</div>
</div>
</div>
</div>
</div>
)}
{/* 换源侧拉面板 */}
<div data-source-panel>
{/* 遮罩层 */} {/* 遮罩层 */}
{showSourcePanel && ( {showEpisodePanel && (
<div <div
className='fixed inset-0 bg-black/50 z-[110]' className='fixed inset-0 bg-black/50 z-[110]'
onClick={() => { onClick={() => {
setShowSourcePanel(false); setShowEpisodePanel(false);
playerContainerRef.current?.focus(); playerContainerRef.current?.focus();
}} }}
/> />
@@ -1381,16 +1326,16 @@ function PlayPageClient() {
{/* 侧拉面板 */} {/* 侧拉面板 */}
<div <div
className={`fixed top-0 right-0 h-full w-full mobile-landscape:w-1/2 md:w-96 bg-black/40 backdrop-blur-xl z-[110] transform transition-transform duration-300 ${ className={`fixed top-0 right-0 h-full w-full mobile-landscape:w-1/2 md:w-80 bg-black/40 backdrop-blur-xl z-[110] transform transition-transform duration-300 ${
showSourcePanel ? 'translate-x-0' : 'translate-x-full' showEpisodePanel ? 'translate-x-0' : 'translate-x-full'
}`} }`}
> >
<div className='p-6 h-full flex flex-col'> <div className='p-6 h-full flex flex-col'>
<div className='flex items-center justify-between mb-6'> <div className='flex items-center justify-between mb-6'>
<h3 className='text-white text-xl font-semibold'></h3> <h3 className='text-white text-xl font-semibold'></h3>
<button <button
onClick={() => { onClick={() => {
setShowSourcePanel(false); setShowEpisodePanel(false);
playerContainerRef.current?.focus(); playerContainerRef.current?.focus();
}} }}
className='text-gray-400 hover:text-white transition-colors' className='text-gray-400 hover:text-white transition-colors'
@@ -1411,215 +1356,283 @@ function PlayPageClient() {
</button> </button>
</div> </div>
{/* 搜索结果 */} <div className='text-gray-300 text-sm mb-4'>
当前: {currentEpisodeIndex + 1} / {totalEpisodes}
</div>
<div className='flex-1 overflow-y-auto'> <div className='flex-1 overflow-y-auto'>
{searchLoading && ( <div className='grid grid-cols-4 gap-3'>
<div className='flex items-center justify-center py-8'> {Array.from({ length: totalEpisodes }, (_, idx) => (
<div className='animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500'></div> <button
<span className='text-gray-300 ml-3'>...</span> key={idx}
</div> onClick={() => handleEpisodeChange(idx)}
)} className={`px-3 py-2 text-sm rounded-lg transition-colors ${
idx === currentEpisodeIndex
{searchError && ( ? 'bg-green-500 text-white'
<div className='text-red-400 text-center py-4'> : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
{searchError} }`}
</div> >
)} {idx + 1}
</button>
{!searchLoading && ))}
!searchError && </div>
searchResults.length === 0 && (
<div className='text-gray-400 text-center py-8'>
</div>
)}
{!searchLoading &&
!searchError &&
searchResults.length > 0 && (
<div className='grid grid-cols-2 gap-4'>
{[
...searchResults.filter(
(r) =>
r.source === currentSource &&
String(r.id) === String(currentId)
),
...searchResults.filter(
(r) =>
!(
r.source === currentSource &&
String(r.id) === String(currentId)
)
),
].map((result) => {
const isCurrentSource =
result.source === currentSource &&
String(result.id) === String(currentId);
return (
<div
key={`${result.source}-${result.id}`}
className={`rounded-lg transition-colors border-2 ${
isCurrentSource
? 'border-green-500 bg-green-500/20 cursor-not-allowed opacity-60'
: 'border-transparent bg-transparent hover:bg-gray-600/30 cursor-pointer'
}`}
onClick={() =>
!isCurrentSource &&
handleSourceChange(
result.source,
result.id,
result.title
)
}
>
{/* 视频封面 */}
<div className='aspect-[2/3] rounded-t-lg overflow-hidden flex items-center justify-center p-1 relative'>
<img
src={result.poster}
alt={result.title}
className='w-full h-full object-cover rounded'
referrerPolicy='no-referrer'
/>
{/* 集数圆形指示器 */}
{result.episodes && (
<div className='absolute top-2 right-2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center'>
<span className='text-white text-xs font-bold'>
{result.episodes}
</span>
</div>
)}
{isCurrentSource && (
<div className='absolute inset-0 flex items-center justify-center pointer-events-none'>
<div className='bg-green-500 text-white text-xs px-3 py-1 rounded shadow-lg'>
</div>
</div>
)}
</div>
{/* 视频信息 */}
<div className='p-2 bg-transparent text-center'>
<h4 className='text-white font-medium text-sm line-clamp-2 mb-2 leading-tight'>
{result.title}
</h4>
<div className='text-gray-400 text-xs space-y-1'>
<div className='inline-block border border-gray-500/60 rounded px-2 py-[1px]'>
{result.source_name}
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)}
{/* 快捷键提示 */} {/* 换源侧拉面板 */}
<div data-source-panel>
{/* 遮罩层 */}
{showSourcePanel && (
<div
className='fixed inset-0 bg-black/50 z-[110]'
onClick={() => {
setShowSourcePanel(false);
playerContainerRef.current?.focus();
}}
/>
)}
{/* 侧拉面板 */}
<div <div
className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 transition-opacity duration-300 ${ className={`fixed top-0 right-0 h-full w-full mobile-landscape:w-1/2 md:w-96 bg-black/40 backdrop-blur-xl z-[110] transform transition-transform duration-300 ${
showShortcutHint ? 'opacity-100' : 'opacity-0 pointer-events-none' showSourcePanel ? 'translate-x-0' : 'translate-x-full'
}`} }`}
> >
<div className='bg-black/80 backdrop-blur-sm rounded p-4 flex items-center space-x-3'> <div className='p-6 h-full flex flex-col'>
<svg <div className='flex items-center justify-between mb-6'>
className='w-6 h-6 text-white' <h3 className='text-white text-xl font-semibold'></h3>
fill='none' <button
stroke='currentColor' onClick={() => {
viewBox='0 0 24 24' setShowSourcePanel(false);
> playerContainerRef.current?.focus();
{shortcutDirection === 'left' && ( }}
<path className='text-gray-400 hover:text-white transition-colors'
strokeLinecap='round' >
strokeLinejoin='round' <svg
strokeWidth='2' className='w-6 h-6'
d='M15 19l-7-7 7-7' fill='none'
></path> stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
{/* 搜索结果 */}
<div className='flex-1 overflow-y-auto'>
{searchLoading && (
<div className='flex items-center justify-center py-8'>
<div className='animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500'></div>
<span className='text-gray-300 ml-3'>...</span>
</div>
)} )}
{shortcutDirection === 'right' && (
<path {searchError && (
strokeLinecap='round' <div className='text-red-400 text-center py-4'>
strokeLinejoin='round' {searchError}
strokeWidth='2' </div>
d='M9 5l7 7-7 7'
></path>
)} )}
{shortcutDirection === 'up' && (
<path {!searchLoading &&
strokeLinecap='round' !searchError &&
strokeLinejoin='round' searchResults.length === 0 && (
strokeWidth='2' <div className='text-gray-400 text-center py-8'>
d='M5 15l7-7 7 7'
></path> </div>
)}
{!searchLoading && !searchError && searchResults.length > 0 && (
<div className='grid grid-cols-2 gap-4'>
{[
...searchResults.filter(
(r) =>
r.source === currentSource &&
String(r.id) === String(currentId)
),
...searchResults.filter(
(r) =>
!(
r.source === currentSource &&
String(r.id) === String(currentId)
)
),
].map((result) => {
const isCurrentSource =
result.source === currentSource &&
String(result.id) === String(currentId);
return (
<div
key={`${result.source}-${result.id}`}
className={`rounded-lg transition-colors border-2 ${
isCurrentSource
? 'border-green-500 bg-green-500/20 cursor-not-allowed opacity-60'
: 'border-transparent bg-transparent hover:bg-gray-600/30 cursor-pointer'
}`}
onClick={() =>
!isCurrentSource &&
handleSourceChange(
result.source,
result.id,
result.title
)
}
>
{/* 视频封面 */}
<div className='aspect-[2/3] rounded-t-lg overflow-hidden flex items-center justify-center p-1 relative'>
<img
src={result.poster}
alt={result.title}
className='w-full h-full object-cover rounded'
referrerPolicy='no-referrer'
/>
{/* 集数圆形指示器 */}
{result.episodes && (
<div className='absolute top-2 right-2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center'>
<span className='text-white text-xs font-bold'>
{result.episodes}
</span>
</div>
)}
{isCurrentSource && (
<div className='absolute inset-0 flex items-center justify-center pointer-events-none'>
<div className='bg-green-500 text-white text-xs px-3 py-1 rounded shadow-lg'>
</div>
</div>
)}
</div>
{/* 视频信息 */}
<div className='p-2 bg-transparent text-center'>
<h4 className='text-white font-medium text-sm line-clamp-2 mb-2 leading-tight'>
{result.title}
</h4>
<div className='text-gray-400 text-xs space-y-1'>
<div className='inline-block border border-gray-500/60 rounded px-2 py-[1px]'>
{result.source_name}
</div>
</div>
</div>
</div>
);
})}
</div>
)} )}
{shortcutDirection === 'down' && ( </div>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M19 9l-7 7-7-7'
></path>
)}
{shortcutDirection === 'play' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M8 5v14l11-7L8 5z'
></path>
)}
{shortcutDirection === 'pause' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M6 6h4v12H6zm8 0h4v12h-4z'
></path>
)}
{shortcutDirection === 'error' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M6 18L18 6M6 6l12 12'
></path>
)}
</svg>
<span className='text-white font-medium'>{shortcutText}</span>
</div> </div>
</div> </div>
</div>
{/* 三倍速提示 */} {/* 快捷键提示 */}
<div <div
className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 transition-opacity duration-300 ${ className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 transition-opacity duration-300 ${
showSpeedTip ? 'opacity-100' : 'opacity-0 pointer-events-none' showShortcutHint ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`} }`}
> >
<div className='bg-black/60 backdrop-blur-sm rounded-lg p-4 flex items-center space-x-3'> <div className='bg-black/80 backdrop-blur-sm rounded p-4 flex items-center space-x-3'>
<svg <svg
className='w-6 h-6 text-white animate-pulse' className='w-6 h-6 text-white'
fill='none' fill='none'
stroke='currentColor' stroke='currentColor'
viewBox='0 0 24 24' viewBox='0 0 24 24'
> >
{shortcutDirection === 'left' && (
<path <path
strokeLinecap='round' strokeLinecap='round'
strokeLinejoin='round' strokeLinejoin='round'
strokeWidth='2' strokeWidth='2'
d='M13 10V3L4 14h7v7l9-11h-7z' d='M15 19l-7-7 7-7'
/> ></path>
</svg> )}
<span className='text-white font-bold text-lg'>3x </span> {shortcutDirection === 'right' && (
</div> <path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M9 5l7 7-7 7'
></path>
)}
{shortcutDirection === 'up' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M5 15l7-7 7 7'
></path>
)}
{shortcutDirection === 'down' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M19 9l-7 7-7-7'
></path>
)}
{shortcutDirection === 'play' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M8 5v14l11-7L8 5z'
></path>
)}
{shortcutDirection === 'pause' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M6 6h4v12H6zm8 0h4v12h-4z'
></path>
)}
{shortcutDirection === 'error' && (
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M6 18L18 6M6 6l12 12'
></path>
)}
</svg>
<span className='text-white font-medium'>{shortcutText}</span>
</div> </div>
</MediaPlayer> </div>
</div>
</> {/* 三倍速提示 */}
<div
className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 transition-opacity duration-300 ${
showSpeedTip ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
>
<div className='bg-black/60 backdrop-blur-sm rounded-lg p-4 flex items-center space-x-3'>
<svg
className='w-6 h-6 text-white animate-pulse'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M13 10V3L4 14h7v7l9-11h-7z'
/>
</svg>
<span className='text-white font-bold text-lg'>3x </span>
</div>
</div>
</MediaPlayer>
</div>
); );
} }
@@ -1680,13 +1693,8 @@ const FavoriteIcon = ({ filled }: { filled: boolean }) => {
export default function PlayPage() { export default function PlayPage() {
return ( return (
<> <Suspense>
<Head> <PlayPageClient />
<meta name='theme-color' content='#000000' /> </Suspense>
</Head>
<Suspense>
<PlayPageClient />
</Suspense>
</>
); );
} }