257 lines
9.7 KiB
TypeScript
257 lines
9.7 KiB
TypeScript
import {
|
||
ArrowDownUp,
|
||
Download,
|
||
Maximize,
|
||
Maximize2,
|
||
Minimize,
|
||
Minimize2,
|
||
Pause,
|
||
Play,
|
||
Repeat1,
|
||
RotateCcw,
|
||
RotateCw,
|
||
Volume2,
|
||
VolumeX,
|
||
} from "lucide-react";
|
||
import type { RefObject } from "react";
|
||
import type { LoopMode, ObjectFit, User } from "../types";
|
||
import { formatRelativeTime, formatTime } from "../utils";
|
||
import { ProgressBar } from "./ProgressBar";
|
||
import { SegmentedProgressBar } from "./SegmentedProgressBar";
|
||
|
||
interface MediaControlsProps {
|
||
isVideo: boolean;
|
||
isPlaying: boolean;
|
||
progress: number;
|
||
volume: number;
|
||
rate: number;
|
||
rotation: number;
|
||
objectFit: ObjectFit;
|
||
loopMode: LoopMode;
|
||
isFullscreen: boolean;
|
||
author: User;
|
||
createdAt: string | Date;
|
||
desc: string;
|
||
// 视频专用
|
||
videoRef?: RefObject<HTMLVideoElement | null>;
|
||
// 图文专用
|
||
currentIndex?: number;
|
||
totalSegments?: number;
|
||
segmentProgress?: number;
|
||
musicUrl?: string | null;
|
||
audioRef?: RefObject<HTMLAudioElement | null>;
|
||
// 回调
|
||
onTogglePlay: () => void;
|
||
onSeek: (ratio: number) => void;
|
||
onVolumeChange: (volume: number) => void;
|
||
onRateChange: (rate: number) => void;
|
||
onRotationChange: (rotation: number) => void;
|
||
onObjectFitChange: (fit: ObjectFit) => void;
|
||
onLoopModeChange: (mode: LoopMode) => void;
|
||
onDownload: () => void;
|
||
onToggleFullscreen: () => void;
|
||
}
|
||
|
||
export function MediaControls({
|
||
isVideo,
|
||
isPlaying,
|
||
progress,
|
||
volume,
|
||
rate,
|
||
rotation,
|
||
objectFit,
|
||
loopMode,
|
||
isFullscreen,
|
||
author,
|
||
createdAt,
|
||
desc,
|
||
videoRef,
|
||
currentIndex = 0,
|
||
totalSegments = 0,
|
||
segmentProgress = 0,
|
||
musicUrl,
|
||
audioRef,
|
||
onTogglePlay,
|
||
onSeek,
|
||
onVolumeChange,
|
||
onRateChange,
|
||
onRotationChange,
|
||
onObjectFitChange,
|
||
onLoopModeChange,
|
||
onDownload,
|
||
onToggleFullscreen,
|
||
}: MediaControlsProps) {
|
||
return (
|
||
<div className="absolute left-0 right-0 bottom-0 px-3 pb-4 pt-2 bg-gradient-to-b from-black/0 via-black/45 to-black/65 flex flex-col gap-2.5">
|
||
{/* 描述行 */}
|
||
<div className="pointer-events-none flex items-center gap-2.5 mb-1">
|
||
<img src={author.avatar_url!} alt="" className="w-8 h-8 rounded-full" />
|
||
<span className="text-[15px] leading-tight text-white/95 drop-shadow">{author.nickname}</span>
|
||
<span className="text-[13px] leading-tight text-white/95 drop-shadow">·</span>
|
||
<span
|
||
className="text-[11px] leading-tight text-white/95 drop-shadow"
|
||
title={new Date(createdAt).toLocaleString()}
|
||
>
|
||
{formatRelativeTime(createdAt)}
|
||
</span>
|
||
</div>
|
||
|
||
{desc ? (
|
||
<div className="pointer-events-none">
|
||
<p className="text-[13px] leading-tight text-white/95 max-h-[3.9em] overflow-hidden [display:-webkit-box] [-webkit-line-clamp:3] [-webkit-box-orient:vertical] drop-shadow">
|
||
{desc}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* 进度条:图文=分段;视频=单段 */}
|
||
{!isVideo && totalSegments > 0 ? (
|
||
<SegmentedProgressBar
|
||
totalSegments={totalSegments}
|
||
currentIndex={currentIndex}
|
||
segmentProgress={segmentProgress}
|
||
onSeek={onSeek}
|
||
/>
|
||
) : (
|
||
<ProgressBar progress={progress} onSeek={onSeek} />
|
||
)}
|
||
|
||
{/* 控制按钮行 - 响应式布局 */}
|
||
<div className="flex items-center justify-between gap-1.5 sm:gap-2.5">
|
||
{/* 左侧:播放控制 + 时间/进度 */}
|
||
<div className="inline-flex items-center gap-1.5 sm:gap-2 min-w-0">
|
||
<button
|
||
className="w-[34px] h-[34px] shrink-0 inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={onTogglePlay}
|
||
aria-label={isPlaying ? "暂停" : "播放"}
|
||
>
|
||
{isPlaying ? <Pause size={18} /> : <Play size={18} />}
|
||
</button>
|
||
|
||
{/* 播放进度显示 - 所有设备都显示 */}
|
||
<div className="text-[13px] text-white/90 font-mono min-w-[70px] sm:min-w-[80px]">
|
||
{isVideo ? (
|
||
(() => {
|
||
const v = videoRef?.current;
|
||
const current = v?.currentTime ?? 0;
|
||
const total = v?.duration ?? 0;
|
||
return total > 0 ? `${formatTime(current)} / ${formatTime(total)}` : "--:-- / --:--";
|
||
})()
|
||
) : (
|
||
`${currentIndex + 1} / ${totalSegments}`
|
||
)}
|
||
</div>
|
||
|
||
{/* 倍速 - 中等屏幕以上显示,仅视频 */}
|
||
{isVideo && (
|
||
<button
|
||
className="hidden md:block h-[30px] px-[10px] rounded-full text-[12px] bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer shrink-0"
|
||
onClick={() => {
|
||
const steps = [1, 1.25, 1.5, 2, 0.75, 0.5];
|
||
const i = steps.indexOf(rate);
|
||
const next = steps[(i + 1) % steps.length];
|
||
onRateChange(next);
|
||
}}
|
||
aria-label="切换倍速"
|
||
>
|
||
{rate}x
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 中间:音量控制 - 中等屏幕以上显示 */}
|
||
<div className="hidden md:inline-flex items-center gap-2 shrink-0">
|
||
{/* 旋转按钮 - 小屏幕以上显示 */}
|
||
<button
|
||
className="hidden sm:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={() => onRotationChange((rotation + 270) % 360)}
|
||
aria-label="向左旋转 90 度"
|
||
title="向左旋转 90 度"
|
||
>
|
||
<RotateCcw size={18} />
|
||
</button>
|
||
<button
|
||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={() => onVolumeChange(volume > 0 ? 0 : 1)}
|
||
aria-label={volume > 0 ? "静音" : "取消静音"}
|
||
>
|
||
{volume > 0 ? <Volume2 size={18} /> : <VolumeX size={18} />}
|
||
</button>
|
||
<input
|
||
type="range"
|
||
min={0}
|
||
max={1}
|
||
step={0.05}
|
||
value={volume}
|
||
onChange={(e) => onVolumeChange(parseFloat(e.target.value))}
|
||
className="w-20 lg:w-28 accent-white cursor-pointer"
|
||
aria-label="音量"
|
||
/>
|
||
<button
|
||
className="hidden sm:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={() => onRotationChange((rotation + 90) % 360)}
|
||
aria-label="向右旋转 90 度"
|
||
title="向右旋转 90 度"
|
||
>
|
||
<RotateCw size={18} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 右侧:功能按钮组 */}
|
||
<div className="inline-flex items-center gap-1 sm:gap-1.5 lg:gap-2 shrink-0">
|
||
{/* 音量按钮 - 仅在小屏幕显示(中等屏幕以上有滑块) */}
|
||
<button
|
||
className="md:hidden w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={() => onVolumeChange(volume > 0 ? 0 : 1)}
|
||
aria-label={volume > 0 ? "静音" : "取消静音"}
|
||
>
|
||
{volume > 0 ? <Volume2 size={18} /> : <VolumeX size={18} />}
|
||
</button>
|
||
|
||
{/* 循环模式 - 中等屏幕以上显示 */}
|
||
<button
|
||
className="hidden md:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={() => onLoopModeChange(loopMode === "loop" ? "sequential" : "loop")}
|
||
aria-label={loopMode === "loop" ? "循环播放" : "顺序播放"}
|
||
title={loopMode === "loop" ? "循环播放" : "顺序播放"}
|
||
>
|
||
{loopMode === "loop" ? <Repeat1 size={18} /> : <ArrowDownUp size={18} />}
|
||
</button>
|
||
|
||
{/* 适配模式 - 小屏幕以上显示 */}
|
||
<button
|
||
className="hidden sm:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={() => onObjectFitChange(objectFit === "contain" ? "cover" : "contain")}
|
||
aria-label={objectFit === "contain" ? "切换到填充模式" : "切换到适应模式"}
|
||
title={objectFit === "contain" ? "切换到填充模式" : "切换到适应模式"}
|
||
>
|
||
{objectFit === "contain" ? <Maximize2 size={18} /> : <Minimize size={18} />}
|
||
</button>
|
||
|
||
{/* 下载 - 中等屏幕以上显示 */}
|
||
<button
|
||
className="hidden md:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={onDownload}
|
||
aria-label={isVideo ? "下载视频" : "下载当前图片"}
|
||
title={isVideo ? "下载视频" : "下载当前图片"}
|
||
>
|
||
<Download size={18} />
|
||
</button>
|
||
|
||
{/* 全屏 - 所有设备都显示 */}
|
||
<button
|
||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||
onClick={onToggleFullscreen}
|
||
aria-label="切换全屏"
|
||
>
|
||
{isFullscreen ? <Minimize2 size={18} /> : <Maximize size={18} />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 图文 BGM(隐藏控件,仅用于播放) */}
|
||
{!isVideo && musicUrl ? <audio ref={audioRef} src={musicUrl} loop preload="metadata" /> : null}
|
||
</div>
|
||
);
|
||
}
|