fix: 合并服务器未推送改动并保留手动视频标签
This commit is contained in:
@@ -1,15 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Play } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Play } from "lucide-react";
|
||||
import type {
|
||||
AwemeData,
|
||||
ImageData,
|
||||
Neighbors,
|
||||
VideoData,
|
||||
VideoTranscript,
|
||||
} from "./types.ts";
|
||||
import { BackgroundCanvas } from "./components/BackgroundCanvas";
|
||||
import { CommentPanel } from "./components/CommentPanel";
|
||||
import { ImageCarousel } from "./components/ImageCarousel";
|
||||
@@ -18,15 +11,37 @@ import { MediaControls } from "./components/MediaControls";
|
||||
import { NavigationButtons } from "./components/NavigationButtons";
|
||||
import { TranscriptPanel } from "./components/TranscriptPanel";
|
||||
import { VideoPlayer } from "./components/VideoPlayer";
|
||||
import { VideoTagEditor } from "./components/VideoTagEditor";
|
||||
import { useBackgroundCanvas } from "./hooks/useBackgroundCanvas";
|
||||
import { useCommentState } from "./hooks/useCommentState";
|
||||
import { useImageCarousel } from "./hooks/useImageCarousel";
|
||||
import { useNavigation } from "./hooks/useNavigation";
|
||||
import { usePlayerState } from "./hooks/usePlayerState";
|
||||
import { useVideoPlayer } from "./hooks/useVideoPlayer";
|
||||
import type {
|
||||
AwemeData,
|
||||
ImageData,
|
||||
Neighbors,
|
||||
VideoData,
|
||||
VideoTranscript,
|
||||
} from "./types.ts";
|
||||
|
||||
const SEGMENT_MS = 4000;
|
||||
|
||||
type WebkitVideoElement = HTMLVideoElement & {
|
||||
webkitEnterFullscreen?: () => void;
|
||||
};
|
||||
|
||||
type MediaPositionStateInput = {
|
||||
duration: number;
|
||||
position: number;
|
||||
playbackRate?: number;
|
||||
};
|
||||
|
||||
type MediaSessionWithPositionState = MediaSession & {
|
||||
setPositionState?: (state: MediaPositionStateInput) => void;
|
||||
};
|
||||
|
||||
interface AwemeDetailClientProps {
|
||||
data: AwemeData;
|
||||
neighbors: Neighbors;
|
||||
@@ -45,6 +60,9 @@ export default function AwemeDetailClient({
|
||||
const playerState = usePlayerState();
|
||||
const commentState = useCommentState();
|
||||
const [transcriptOpen, setTranscriptOpen] = useState(false);
|
||||
const [contentTags, setContentTags] = useState<string[]>(
|
||||
isVideo ? (data as VideoData).content_tags : [],
|
||||
);
|
||||
|
||||
// 引用
|
||||
const mediaContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -68,6 +86,7 @@ export default function AwemeDetailClient({
|
||||
|
||||
// 图文轮播状态
|
||||
const images = isVideo ? [] : (data as ImageData).images;
|
||||
const imageCount = images.length;
|
||||
const imageCarouselState = useImageCarousel({
|
||||
images,
|
||||
isPlaying: playerState.isPlaying,
|
||||
@@ -80,40 +99,42 @@ export default function AwemeDetailClient({
|
||||
});
|
||||
|
||||
// 视频播放器 hooks
|
||||
if (isVideo) {
|
||||
useVideoPlayer({
|
||||
awemeId: data.aweme_id,
|
||||
videoRef,
|
||||
volume: playerState.volume,
|
||||
rate: playerState.rate,
|
||||
loopMode: playerState.loopMode,
|
||||
neighbors,
|
||||
progressRestored: playerState.progressRestored,
|
||||
setIsPlaying: playerState.setIsPlaying,
|
||||
setProgress: playerState.setProgress,
|
||||
setProgressRestored: playerState.setProgressRestored,
|
||||
});
|
||||
}
|
||||
useVideoPlayer({
|
||||
enabled: isVideo,
|
||||
awemeId: data.aweme_id,
|
||||
videoRef,
|
||||
volume: playerState.volume,
|
||||
rate: playerState.rate,
|
||||
loopMode: playerState.loopMode,
|
||||
neighbors,
|
||||
progressRestored: playerState.progressRestored,
|
||||
setIsPlaying: playerState.setIsPlaying,
|
||||
setProgress: playerState.setProgress,
|
||||
setProgressRestored: playerState.setProgressRestored,
|
||||
});
|
||||
|
||||
// 媒体操作函数
|
||||
const seekTo = (ratio: number) => {
|
||||
ratio = Math.min(1, Math.max(0, ratio));
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (!v || !v.duration) return;
|
||||
v.currentTime = v.duration * ratio;
|
||||
return;
|
||||
}
|
||||
if (!images?.length) return;
|
||||
imageCarouselState.seekTo(ratio);
|
||||
};
|
||||
const seekTo = useCallback(
|
||||
(ratio: number) => {
|
||||
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (!v || !v.duration) return;
|
||||
v.currentTime = v.duration * clampedRatio;
|
||||
return;
|
||||
}
|
||||
if (!imageCount) return;
|
||||
imageCarouselState.seekTo(clampedRatio);
|
||||
},
|
||||
[imageCarouselState.seekTo, imageCount, isVideo],
|
||||
);
|
||||
|
||||
const seekImageByVisualRatio = (ratio: number) => {
|
||||
if (!images?.length) return;
|
||||
if (!imageCount) return;
|
||||
|
||||
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
||||
const rawSegment = clampedRatio * images.length;
|
||||
const targetIndex = Math.min(images.length - 1, Math.floor(rawSegment));
|
||||
const rawSegment = clampedRatio * imageCount;
|
||||
const targetIndex = Math.min(imageCount - 1, Math.floor(rawSegment));
|
||||
const segmentProgress = Math.min(1, Math.max(0, rawSegment - targetIndex));
|
||||
imageCarouselState.goToIndex(targetIndex, segmentProgress);
|
||||
};
|
||||
@@ -152,14 +173,12 @@ export default function AwemeDetailClient({
|
||||
document.body.requestFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
const vRef = videoRef.current;
|
||||
if (vRef && vRef.requestFullscreen) {
|
||||
const vRef = videoRef.current as WebkitVideoElement | null;
|
||||
if (vRef?.requestFullscreen) {
|
||||
vRef.requestFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (vRef && vRef.webkitEnterFullscreen) {
|
||||
// @ts-ignore
|
||||
if (vRef?.webkitEnterFullscreen) {
|
||||
vRef.webkitEnterFullscreen();
|
||||
}
|
||||
} else {
|
||||
@@ -167,20 +186,20 @@ export default function AwemeDetailClient({
|
||||
}
|
||||
};
|
||||
|
||||
const prevImg = () => {
|
||||
if (!images?.length) return;
|
||||
const prevImg = useCallback(() => {
|
||||
if (!imageCount) return;
|
||||
const next = Math.max(0, imageCarouselState.idxRef.current - 1);
|
||||
imageCarouselState.goToIndex(next);
|
||||
};
|
||||
}, [imageCarouselState.goToIndex, imageCarouselState.idxRef, imageCount]);
|
||||
|
||||
const nextImg = () => {
|
||||
if (!images?.length) return;
|
||||
const nextImg = useCallback(() => {
|
||||
if (!imageCount) return;
|
||||
const next = Math.min(
|
||||
images.length - 1,
|
||||
imageCount - 1,
|
||||
imageCarouselState.idxRef.current + 1,
|
||||
);
|
||||
imageCarouselState.goToIndex(next);
|
||||
};
|
||||
}, [imageCarouselState.goToIndex, imageCarouselState.idxRef, imageCount]);
|
||||
|
||||
const handleDownload = () => {
|
||||
if (isVideo) {
|
||||
@@ -249,7 +268,7 @@ export default function AwemeDetailClient({
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("mediaSession" in navigator)) return;
|
||||
|
||||
const ms = (navigator as any).mediaSession as MediaSession;
|
||||
const ms = navigator.mediaSession as MediaSessionWithPositionState;
|
||||
|
||||
// 元信息:标题、作者、专辑(使用创建时间),封面图(优先当前图片,其次作者头像)
|
||||
const title = data.desc || `作品 ${data.aweme_id}`;
|
||||
@@ -282,12 +301,12 @@ export default function AwemeDetailClient({
|
||||
|
||||
const updatePosition = () => {
|
||||
try {
|
||||
// @ts-ignore: setPositionState 可能不存在
|
||||
if (!ms.setPositionState) return;
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (!v || !isFinite(v.duration) || isNaN(v.duration)) return;
|
||||
// @ts-ignore
|
||||
if (!v || !Number.isFinite(v.duration) || Number.isNaN(v.duration)) {
|
||||
return;
|
||||
}
|
||||
ms.setPositionState({
|
||||
duration: Math.max(0, v.duration),
|
||||
position: Math.max(0, v.currentTime),
|
||||
@@ -300,14 +319,12 @@ export default function AwemeDetailClient({
|
||||
0,
|
||||
Math.min(duration, (playerState.progress * totalMs) / 1000),
|
||||
);
|
||||
// @ts-ignore
|
||||
ms.setPositionState({ duration, position, playbackRate: 1 });
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// 绑定视频事件以同步状态
|
||||
let timeUpdateTimer: number | null = null;
|
||||
const v = videoRef.current;
|
||||
if (isVideo && v) {
|
||||
const onPlay = () => {
|
||||
@@ -331,7 +348,6 @@ export default function AwemeDetailClient({
|
||||
updatePosition();
|
||||
|
||||
// 清理
|
||||
timeUpdateTimer = null;
|
||||
return () => {
|
||||
v.removeEventListener("play", onPlay);
|
||||
v.removeEventListener("pause", onPause);
|
||||
@@ -346,15 +362,10 @@ export default function AwemeDetailClient({
|
||||
}
|
||||
// 依赖:当这些变化时刷新元信息与位置状态
|
||||
}, [
|
||||
isVideo,
|
||||
data.aweme_id,
|
||||
data.desc,
|
||||
data.author.nickname,
|
||||
data.author.avatar_url,
|
||||
data.created_at,
|
||||
imageCarouselState.idx,
|
||||
data,
|
||||
imageCarouselState.totalDurationMs,
|
||||
images,
|
||||
isVideo,
|
||||
playerState.isPlaying,
|
||||
playerState.progress,
|
||||
playerState.rate,
|
||||
@@ -364,7 +375,7 @@ export default function AwemeDetailClient({
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("mediaSession" in navigator)) return;
|
||||
const ms = (navigator as any).mediaSession as MediaSession;
|
||||
const ms = navigator.mediaSession;
|
||||
|
||||
const getImagesTotalMs = () => imageCarouselState.totalDurationMs;
|
||||
|
||||
@@ -421,7 +432,7 @@ export default function AwemeDetailClient({
|
||||
const handleSeekTo = (seekTimeSec: number) => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (v && isFinite(v.duration)) {
|
||||
if (v && Number.isFinite(v.duration)) {
|
||||
v.currentTime = Math.max(0, Math.min(v.duration, seekTimeSec));
|
||||
}
|
||||
} else if (images?.length) {
|
||||
@@ -434,13 +445,13 @@ export default function AwemeDetailClient({
|
||||
ms.setActionHandler("play", handlePlay);
|
||||
ms.setActionHandler("pause", handlePause);
|
||||
ms.setActionHandler("stop", handleStop);
|
||||
ms.setActionHandler("seekbackward", (details: any) =>
|
||||
ms.setActionHandler("seekbackward", (details: MediaSessionActionDetails) =>
|
||||
handleSeekDelta(-((details?.seekOffset as number) || 10)),
|
||||
);
|
||||
ms.setActionHandler("seekforward", (details: any) =>
|
||||
ms.setActionHandler("seekforward", (details: MediaSessionActionDetails) =>
|
||||
handleSeekDelta((details?.seekOffset as number) || 10),
|
||||
);
|
||||
ms.setActionHandler("seekto", (details: any) => {
|
||||
ms.setActionHandler("seekto", (details: MediaSessionActionDetails) => {
|
||||
if (typeof details?.seekTime === "number") handleSeekTo(details.seekTime);
|
||||
});
|
||||
ms.setActionHandler("previoustrack", () => {
|
||||
@@ -477,8 +488,12 @@ export default function AwemeDetailClient({
|
||||
neighbors.next,
|
||||
router,
|
||||
playerState.progress,
|
||||
playerState.setIsPlaying,
|
||||
pauseImageMedia,
|
||||
imageCarouselState.totalDurationMs,
|
||||
nextImg,
|
||||
prevImg,
|
||||
seekTo,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -537,6 +552,14 @@ export default function AwemeDetailClient({
|
||||
)}
|
||||
|
||||
{/* 媒体控制栏 */}
|
||||
{isVideo && (
|
||||
<VideoTagEditor
|
||||
awemeId={data.aweme_id}
|
||||
tags={contentTags}
|
||||
onTagsChange={setContentTags}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MediaControls
|
||||
isVideo={isVideo}
|
||||
isPlaying={playerState.isPlaying}
|
||||
|
||||
Reference in New Issue
Block a user