forked from 77media/video-flow
兼容 竖屏 样式
This commit is contained in:
parent
735d90bbd7
commit
93a7b2e68a
@ -367,6 +367,8 @@ export interface VideoFlowProjectResponse {
|
|||||||
final_simple_video: string;
|
final_simple_video: string;
|
||||||
/** 最终视频 */
|
/** 最终视频 */
|
||||||
final_video: string;
|
final_video: string;
|
||||||
|
/** 画面比例 */
|
||||||
|
aspect_ratio: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 新角色列表项接口
|
* 新角色列表项接口
|
||||||
|
|||||||
@ -50,7 +50,7 @@ import { PcTemplateModal } from "./PcTemplateModal";
|
|||||||
import { H5TemplateDrawer } from "./H5TemplateDrawer";
|
import { H5TemplateDrawer } from "./H5TemplateDrawer";
|
||||||
import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
|
import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
|
||||||
import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
|
import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
|
||||||
import { AspectRatioSelector } from "./AspectRatioSelector";
|
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
|
||||||
|
|
||||||
const LauguageOptions = [
|
const LauguageOptions = [
|
||||||
{ value: "english", label: "English", isVip: false, code:'EN' },
|
{ value: "english", label: "English", isVip: false, code:'EN' },
|
||||||
@ -131,7 +131,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
language: string;
|
language: string;
|
||||||
videoDuration: string;
|
videoDuration: string;
|
||||||
expansion_mode: boolean;
|
expansion_mode: boolean;
|
||||||
aspect_ratio: "VIDEO_ASPECT_RATIO_LANDSCAPE" | "VIDEO_ASPECT_RATIO_PORTRAIT";
|
aspect_ratio: AspectRatioValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [configOptions, setConfigOptions] = useState<ConfigOptions>({
|
const [configOptions, setConfigOptions] = useState<ConfigOptions>({
|
||||||
|
|||||||
@ -29,6 +29,8 @@ export default function CreateToVideo2() {
|
|||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [userId, setUserId] = useState<number>(0);
|
const [userId, setUserId] = useState<number>(0);
|
||||||
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
||||||
|
/** 保存每个项目的视频朝向 */
|
||||||
|
const [orientationMap, setOrientationMap] = useState<Record<string, 'portrait' | 'landscape'>>({});
|
||||||
|
|
||||||
// 添加一个 ref 来跟踪当前正在加载的页码
|
// 添加一个 ref 来跟踪当前正在加载的页码
|
||||||
const loadingPageRef = useRef<number | null>(null);
|
const loadingPageRef = useRef<number | null>(null);
|
||||||
@ -36,46 +38,14 @@ export default function CreateToVideo2() {
|
|||||||
// 在客户端挂载后读取localStorage
|
// 在客户端挂载后读取localStorage
|
||||||
// 监听滚动事件,实现无限加载
|
// 监听滚动事件,实现无限加载
|
||||||
// 修改滚动处理函数,添加节流
|
// 修改滚动处理函数,添加节流
|
||||||
const handleScroll = useCallback(() => {
|
// 修改获取剧集列表函数(提前定义,供滚动与初始化调用)
|
||||||
if (!scrollContainerRef.current || !hasMore || isLoadingMore || isLoading) return;
|
const getEpisodeList = useCallback(async (userId: number, page: number = 1, loadMore: boolean = false) => {
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
|
||||||
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
||||||
// 直接使用 currentPage,不再使用 setCurrentPage 的回调
|
|
||||||
const nextPage = currentPage + 1;
|
|
||||||
if (nextPage <= totalPages) {
|
|
||||||
getEpisodeList(userId, nextPage, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
|
||||||
console.log('currentUser', currentUser);
|
|
||||||
setUserId(currentUser.id);
|
|
||||||
getEpisodeList(currentUser.id, 1, false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 添加滚动监听
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollContainer = scrollContainerRef.current;
|
|
||||||
if (scrollContainer) {
|
|
||||||
scrollContainer.addEventListener('scroll', handleScroll);
|
|
||||||
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
|
||||||
}
|
|
||||||
}, [handleScroll]);
|
|
||||||
|
|
||||||
// 修改获取剧集列表函数
|
|
||||||
const getEpisodeList = async (userId: number, page: number = 1, loadMore: boolean = false) => {
|
|
||||||
// 检查是否正在加载该页
|
// 检查是否正在加载该页
|
||||||
if (loadingPageRef.current === page) return;
|
if (loadingPageRef.current === page) return;
|
||||||
if (isLoading || (isLoadingMore && !loadMore)) return;
|
|
||||||
|
|
||||||
// 设置当前正在加载的页码
|
// 设置当前正在加载的页码
|
||||||
loadingPageRef.current = page;
|
loadingPageRef.current = page;
|
||||||
|
|
||||||
if (loadMore) {
|
if (loadMore) {
|
||||||
setIsLoadingMore(true);
|
setIsLoadingMore(true);
|
||||||
} else {
|
} else {
|
||||||
@ -93,7 +63,7 @@ export default function CreateToVideo2() {
|
|||||||
|
|
||||||
if (episodeListResponse.code === 0) {
|
if (episodeListResponse.code === 0) {
|
||||||
const { movie_projects, total_pages } = episodeListResponse.data;
|
const { movie_projects, total_pages } = episodeListResponse.data;
|
||||||
|
|
||||||
// 确保数据不重复
|
// 确保数据不重复
|
||||||
if (loadMore) {
|
if (loadMore) {
|
||||||
setEpisodeList(prev => {
|
setEpisodeList(prev => {
|
||||||
@ -105,7 +75,7 @@ export default function CreateToVideo2() {
|
|||||||
} else {
|
} else {
|
||||||
setEpisodeList(movie_projects);
|
setEpisodeList(movie_projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalPages(total_pages);
|
setTotalPages(total_pages);
|
||||||
setHasMore(page < total_pages);
|
setHasMore(page < total_pages);
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@ -119,7 +89,39 @@ export default function CreateToVideo2() {
|
|||||||
// 清除当前加载页码
|
// 清除当前加载页码
|
||||||
loadingPageRef.current = null;
|
loadingPageRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
}, [perPage]);
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!scrollContainerRef.current || !hasMore || isLoadingMore || isLoading) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 100) {
|
||||||
|
// 直接使用 currentPage,不再使用 setCurrentPage 的回调
|
||||||
|
const nextPage = currentPage + 1;
|
||||||
|
if (nextPage <= totalPages) {
|
||||||
|
getEpisodeList(userId, nextPage, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [hasMore, isLoadingMore, isLoading, totalPages, userId, currentPage, getEpisodeList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
|
||||||
|
console.log('currentUser', currentUser);
|
||||||
|
setUserId(currentUser.id);
|
||||||
|
getEpisodeList(currentUser.id, 1, false);
|
||||||
|
}
|
||||||
|
}, [getEpisodeList]);
|
||||||
|
|
||||||
|
// 添加滚动监听
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.addEventListener('scroll', handleScroll);
|
||||||
|
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
// 原函数位置已上移
|
||||||
|
|
||||||
// 视频上传和创建功能已迁移到ChatInputBox组件中
|
// 视频上传和创建功能已迁移到ChatInputBox组件中
|
||||||
|
|
||||||
@ -188,19 +190,38 @@ export default function CreateToVideo2() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据元数据记录视频朝向(横/竖)
|
||||||
|
* @param {string} projectId - 项目唯一标识
|
||||||
|
* @param {HTMLVideoElement} videoEl - 视频元素
|
||||||
|
*/
|
||||||
|
const handleVideoMetadata = (projectId: string, videoEl: HTMLVideoElement) => {
|
||||||
|
const { videoWidth, videoHeight } = videoEl;
|
||||||
|
if (!videoWidth || !videoHeight) return;
|
||||||
|
setOrientationMap((prev) => {
|
||||||
|
if (prev[projectId]) return prev;
|
||||||
|
const next = videoWidth >= videoHeight ? 'landscape' : 'portrait';
|
||||||
|
return { ...prev, [projectId]: next };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderProjectCard = (project: any) => {
|
const renderProjectCard = (project: any) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LazyLoad once>
|
<LazyLoad once>
|
||||||
<div
|
<div
|
||||||
key={project.project_id}
|
key={project.project_id}
|
||||||
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
|
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300 mb-6 break-inside-avoid"
|
||||||
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
onMouseEnter={() => handleMouseEnter(project.project_id)}
|
||||||
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
onMouseLeave={() => handleMouseLeave(project.project_id)}
|
||||||
data-alt="project-card"
|
data-alt="project-card"
|
||||||
>
|
>
|
||||||
{/* 视频/图片区域 */}
|
{/* 视频/图片区域 */}
|
||||||
<div className="relative aspect-video" onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}>
|
<div
|
||||||
|
className={`relative ${orientationMap[project.project_id] === 'portrait' ? 'aspect-[9/16]' : 'aspect-video'}`}
|
||||||
|
onClick={() => router.push(`/movies/work-flow?episodeId=${project.project_id}`)}
|
||||||
|
data-alt="media-container"
|
||||||
|
>
|
||||||
{(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (
|
{(project.final_video_url || project.final_simple_video_url || project.video_urls) ? (
|
||||||
<video
|
<video
|
||||||
ref={(el) => setVideoRef(project.project_id, el)}
|
ref={(el) => setVideoRef(project.project_id, el)}
|
||||||
@ -209,7 +230,8 @@ export default function CreateToVideo2() {
|
|||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="none"
|
preload="metadata"
|
||||||
|
onLoadedMetadata={(e) => handleVideoMetadata(project.project_id, e.currentTarget)}
|
||||||
poster={
|
poster={
|
||||||
getFirstFrame(project.final_video_url || project.final_simple_video_url || project.video_urls)
|
getFirstFrame(project.final_video_url || project.final_simple_video_url || project.video_urls)
|
||||||
}
|
}
|
||||||
@ -259,7 +281,7 @@ export default function CreateToVideo2() {
|
|||||||
</Tooltip> */}
|
</Tooltip> */}
|
||||||
</div>
|
</div>
|
||||||
{/* TODO 删除 */}
|
{/* TODO 删除 */}
|
||||||
{/* <Tooltip title="Delete">
|
{/* <Tooltip title="Delete"></Tooltip>
|
||||||
<Button size="small" type="text" className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" /></Button>
|
<Button size="small" type="text" className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" /></Button>
|
||||||
</Tooltip> */}
|
</Tooltip> */}
|
||||||
</div>
|
</div>
|
||||||
@ -282,7 +304,7 @@ export default function CreateToVideo2() {
|
|||||||
{episodeList.length > 0 && (
|
{episodeList.length > 0 && (
|
||||||
/* 优化的剧集网格 */
|
/* 优化的剧集网格 */
|
||||||
<div className="pb-8">
|
<div className="pb-8">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="columns-1 sm:columns-2 lg:columns-3" style={{ columnGap: '1.5rem' }} data-alt="masonry-grid">
|
||||||
{episodeList.map(renderProjectCard)}
|
{episodeList.map(renderProjectCard)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -222,7 +222,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
showGotoCutButton,
|
showGotoCutButton,
|
||||||
generateEditPlan,
|
generateEditPlan,
|
||||||
handleRetryVideo,
|
handleRetryVideo,
|
||||||
isShowAutoEditing
|
isShowAutoEditing,
|
||||||
|
aspectRatio
|
||||||
} = useWorkflowData({
|
} = useWorkflowData({
|
||||||
onEditPlanGenerated: handleEditPlanGenerated,
|
onEditPlanGenerated: handleEditPlanGenerated,
|
||||||
editingStatus: editingStatus,
|
editingStatus: editingStatus,
|
||||||
@ -537,6 +538,7 @@ Please process this video editing request.`;
|
|||||||
onRetryVideo={handleRetryVideo}
|
onRetryVideo={handleRetryVideo}
|
||||||
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')}
|
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -622,7 +624,7 @@ Please process this video editing request.`;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 智能对话弹窗 */}
|
{/* 智能对话弹窗 */}
|
||||||
<Drawer
|
{/* <Drawer
|
||||||
width="25%"
|
width="25%"
|
||||||
placement="right"
|
placement="right"
|
||||||
closable={false}
|
closable={false}
|
||||||
@ -662,7 +664,7 @@ Please process this video editing request.`;
|
|||||||
}}
|
}}
|
||||||
aiEditingResult={aiEditingResult}
|
aiEditingResult={aiEditingResult}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer> */}
|
||||||
|
|
||||||
<EditModal
|
<EditModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
|
|||||||
@ -164,7 +164,7 @@ export function H5MediaViewer({
|
|||||||
|
|
||||||
// 渲染视频 slide
|
// 渲染视频 slide
|
||||||
const renderVideoSlides = () => (
|
const renderVideoSlides = () => (
|
||||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-video min-h-[200px] overflow-hidden rounded-lg">
|
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg">
|
||||||
<Carousel
|
<Carousel
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
key={`h5-carousel-video-${stage}-${videoUrls.length}`}
|
key={`h5-carousel-video-${stage}-${videoUrls.length}`}
|
||||||
@ -187,7 +187,7 @@ export function H5MediaViewer({
|
|||||||
<>
|
<>
|
||||||
<video
|
<video
|
||||||
ref={(el) => (videoRefs.current[idx] = el)}
|
ref={(el) => (videoRefs.current[idx] = el)}
|
||||||
className="w-full h-full object-cover [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
|
className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
|
||||||
src={url}
|
src={url}
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
playsInline
|
playsInline
|
||||||
@ -248,7 +248,7 @@ export function H5MediaViewer({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full aspect-video min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status">
|
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status">
|
||||||
{status === 0 && (
|
{status === 0 && (
|
||||||
<span className="text-blue-500 text-base">Generating...</span>
|
<span className="text-blue-500 text-base">Generating...</span>
|
||||||
)}
|
)}
|
||||||
@ -285,7 +285,7 @@ export function H5MediaViewer({
|
|||||||
|
|
||||||
// 渲染图片 slide
|
// 渲染图片 slide
|
||||||
const renderImageSlides = () => (
|
const renderImageSlides = () => (
|
||||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-video min-h-[200px] overflow-hidden rounded-lg">
|
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg">
|
||||||
<Carousel
|
<Carousel
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
key={`h5-carousel-image-${stage}-${imageUrls.length}`}
|
key={`h5-carousel-image-${stage}-${imageUrls.length}`}
|
||||||
@ -299,7 +299,7 @@ export function H5MediaViewer({
|
|||||||
>
|
>
|
||||||
{imageUrls.map((url, idx) => (
|
{imageUrls.map((url, idx) => (
|
||||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
|
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
|
||||||
<img src={url} alt="scene" className="w-full h-full object-cover" />
|
<img src={url} alt="scene" className="w-full h-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
@ -416,7 +416,7 @@ export function H5MediaViewer({
|
|||||||
data-alt="final-thumb-item"
|
data-alt="final-thumb-item"
|
||||||
aria-label="Select final video"
|
aria-label="Select final video"
|
||||||
>
|
>
|
||||||
<img src={getFirstFrame(taskObject.final.url)} alt="final" className="w-full h-auto object-cover" />
|
<img src={getFirstFrame(taskObject.final.url)} alt="final" className="w-full h-auto object-contain" />
|
||||||
<div className="text-[10px] text-white/80 text-center py-0.5">Final</div>
|
<div className="text-[10px] text-white/80 text-center py-0.5">Final</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -176,7 +176,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
ref={finalVideoRef}
|
ref={finalVideoRef}
|
||||||
className="w-full h-full object-cover rounded-lg"
|
className="w-full h-full object-contain rounded-lg"
|
||||||
src={taskObject.final.url}
|
src={taskObject.final.url}
|
||||||
autoPlay={isFinalVideoPlaying}
|
autoPlay={isFinalVideoPlaying}
|
||||||
loop
|
loop
|
||||||
@ -345,7 +345,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
className="w-full h-full rounded-lg object-cover object-center"
|
className="w-full h-full rounded-lg object-contain object-center"
|
||||||
src={taskObject.final.url}
|
src={taskObject.final.url}
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
@ -490,7 +490,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
<video
|
<video
|
||||||
ref={mainVideoRef}
|
ref={mainVideoRef}
|
||||||
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||||
className="w-full h-full rounded-lg object-cover object-center relative z-10"
|
className="w-full h-full rounded-lg object-contain object-center relative z-10"
|
||||||
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||||
poster={getFirstFrame(taskObject.videos.data[currentSketchIndex].urls[0])}
|
poster={getFirstFrame(taskObject.videos.data[currentSketchIndex].urls[0])}
|
||||||
preload="none"
|
preload="none"
|
||||||
@ -655,7 +655,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
<motion.img
|
<motion.img
|
||||||
key={currentSketch.url}
|
key={currentSketch.url}
|
||||||
src={currentSketch.url}
|
src={currentSketch.url}
|
||||||
className="w-full h-full rounded-lg object-cover"
|
className="w-full h-full rounded-lg object-contain"
|
||||||
// 用 circle clip-path 实现“扩散”
|
// 用 circle clip-path 实现“扩散”
|
||||||
initial={{ clipPath: "circle(0% at 50% 50%)" }}
|
initial={{ clipPath: "circle(0% at 50% 50%)" }}
|
||||||
animate={{ clipPath: "circle(150% at 50% 50%)" }}
|
animate={{ clipPath: "circle(150% at 50% 50%)" }}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
|||||||
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw, CircleAlert } from 'lucide-react';
|
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw, CircleAlert } from 'lucide-react';
|
||||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||||
import { getFirstFrame } from '@/utils/tools';
|
import { getFirstFrame } from '@/utils/tools';
|
||||||
|
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||||
|
|
||||||
interface ThumbnailGridProps {
|
interface ThumbnailGridProps {
|
||||||
isDisabledFocus: boolean;
|
isDisabledFocus: boolean;
|
||||||
@ -16,6 +17,7 @@ interface ThumbnailGridProps {
|
|||||||
onRetryVideo: (video_id: string) => void;
|
onRetryVideo: (video_id: string) => void;
|
||||||
className: string;
|
className: string;
|
||||||
selectedView?: 'final' | 'video' | null;
|
selectedView?: 'final' | 'video' | null;
|
||||||
|
aspectRatio: AspectRatioValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,7 +30,8 @@ export function ThumbnailGrid({
|
|||||||
onSketchSelect,
|
onSketchSelect,
|
||||||
onRetryVideo,
|
onRetryVideo,
|
||||||
className,
|
className,
|
||||||
selectedView
|
selectedView,
|
||||||
|
aspectRatio
|
||||||
}: ThumbnailGridProps) {
|
}: ThumbnailGridProps) {
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
@ -186,8 +189,10 @@ export function ThumbnailGrid({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`video-${urls}-${index}`}
|
key={`video-${urls}-${index}`}
|
||||||
className={`relative aspect-video rounded-lg overflow-hidden
|
className={`relative aspect-auto rounded-lg overflow-hidden
|
||||||
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
||||||
|
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[200px]' : 'min-w-[70px]'}
|
||||||
|
`}
|
||||||
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
||||||
>
|
>
|
||||||
|
|
||||||
@ -208,7 +213,7 @@ export function ThumbnailGrid({
|
|||||||
|
|
||||||
{taskObject.videos.data[index].urls && taskObject.videos.data[index].urls.length > 0 ? (
|
{taskObject.videos.data[index].urls && taskObject.videos.data[index].urls.length > 0 ? (
|
||||||
// <video
|
// <video
|
||||||
// className="w-full h-full object-cover"
|
// className="w-full h-full object-contain"
|
||||||
// src={taskObject.videos.data[index].urls[0]}
|
// src={taskObject.videos.data[index].urls[0]}
|
||||||
// playsInline
|
// playsInline
|
||||||
// loop
|
// loop
|
||||||
@ -220,14 +225,14 @@ export function ThumbnailGrid({
|
|||||||
onMouseLeave={() => handleMouseLeave(index)}
|
onMouseLeave={() => handleMouseLeave(index)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-contain"
|
||||||
src={getFirstFrame(taskObject.videos.data[index].urls[0])}
|
src={getFirstFrame(taskObject.videos.data[index].urls[0])}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
alt="video thumbnail"
|
alt="video thumbnail"
|
||||||
/>
|
/>
|
||||||
{hoveredIndex === index && (
|
{hoveredIndex === index && (
|
||||||
<video
|
<video
|
||||||
className="absolute inset-0 w-full h-full object-cover"
|
className="absolute inset-0 w-full h-full object-contain"
|
||||||
src={taskObject.videos.data[index].urls[0]}
|
src={taskObject.videos.data[index].urls[0]}
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
@ -249,7 +254,7 @@ export function ThumbnailGrid({
|
|||||||
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
||||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
|
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
|
||||||
<Video className="w-3 h-3 text-green-400 mr-1" />
|
<Video className="w-3 h-3 text-green-400 mr-1" />
|
||||||
<span className="text-xs text-green-400">Shot {index + 1}</span>
|
<span className="text-xs text-green-400">{index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -269,8 +274,10 @@ export function ThumbnailGrid({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
||||||
className={`relative aspect-video rounded-lg overflow-hidden
|
className={`relative aspect-auto rounded-lg overflow-hidden
|
||||||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
||||||
|
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[200px]' : 'min-w-[70px]'}
|
||||||
|
`}
|
||||||
onClick={() => !isDragging && onSketchSelect(index)}
|
onClick={() => !isDragging && onSketchSelect(index)}
|
||||||
>
|
>
|
||||||
|
|
||||||
@ -293,7 +300,7 @@ export function ThumbnailGrid({
|
|||||||
{(sketch.status === 1) && (
|
{(sketch.status === 1) && (
|
||||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||||
<img
|
<img
|
||||||
className="w-full h-full object-cover select-none"
|
className="w-full h-full object-contain select-none"
|
||||||
src={sketch.url}
|
src={sketch.url}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
alt={sketch.type ? String(sketch.type) : 'sketch'}
|
alt={sketch.type ? String(sketch.type) : 'sketch'}
|
||||||
@ -319,7 +326,7 @@ export function ThumbnailGrid({
|
|||||||
{(!sketch.type || sketch.type === 'shot_sketch') && (
|
{(!sketch.type || sketch.type === 'shot_sketch') && (
|
||||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm">
|
<div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm">
|
||||||
<Clapperboard className="w-3 h-3 text-cyan-400 mr-1" />
|
<Clapperboard className="w-3 h-3 text-cyan-400 mr-1" />
|
||||||
<span className="text-xs text-cyan-400">Shot {index + 1}</span>
|
<span className="text-xs text-cyan-400">{index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -337,7 +344,7 @@ export function ThumbnailGrid({
|
|||||||
<div
|
<div
|
||||||
ref={thumbnailsRef}
|
ref={thumbnailsRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none ${className}`}
|
className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none ${className} auto-cols-max`}
|
||||||
autoFocus
|
autoFocus
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovi
|
|||||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||||
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||||
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
||||||
|
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||||
|
|
||||||
interface UseWorkflowDataProps {
|
interface UseWorkflowDataProps {
|
||||||
onEditPlanGenerated?: () => void;
|
onEditPlanGenerated?: () => void;
|
||||||
@ -91,6 +92,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
const [isLoadingGenerateEditPlan, setIsLoadingGenerateEditPlan] = useState(false);
|
const [isLoadingGenerateEditPlan, setIsLoadingGenerateEditPlan] = useState(false);
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
|
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
|
||||||
|
aspectRatio: 'VIDEO_ASPECT_RATIO_LANDSCAPE' as AspectRatioValue,
|
||||||
originalText: '',
|
originalText: '',
|
||||||
isLoading: true
|
isLoading: true
|
||||||
});
|
});
|
||||||
@ -529,6 +531,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
try {
|
try {
|
||||||
setState({
|
setState({
|
||||||
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
|
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
|
||||||
|
aspectRatio: 'VIDEO_ASPECT_RATIO_LANDSCAPE' as AspectRatioValue,
|
||||||
originalText: '',
|
originalText: '',
|
||||||
isLoading: true
|
isLoading: true
|
||||||
});
|
});
|
||||||
@ -540,7 +543,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status, data, tags, mode, original_text, title, name, final_simple_video, final_video } = response.data;
|
const { status, data, tags, mode, original_text, aspect_ratio, name, final_simple_video, final_video } = response.data;
|
||||||
|
|
||||||
const { current: taskCurrent } = tempTaskObject;
|
const { current: taskCurrent } = tempTaskObject;
|
||||||
|
|
||||||
@ -681,6 +684,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
|
|
||||||
setState({
|
setState({
|
||||||
mode: mode as 'automatic' | 'manual' | 'auto',
|
mode: mode as 'automatic' | 'manual' | 'auto',
|
||||||
|
aspectRatio: aspect_ratio as AspectRatioValue,
|
||||||
originalText: original_text,
|
originalText: original_text,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
});
|
});
|
||||||
@ -701,6 +705,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
setNeedStreamData(true);
|
setNeedStreamData(true);
|
||||||
setState({
|
setState({
|
||||||
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
|
mode: 'automatic' as 'automatic' | 'manual' | 'auto',
|
||||||
|
aspectRatio: 'VIDEO_ASPECT_RATIO_LANDSCAPE' as AspectRatioValue,
|
||||||
originalText: '',
|
originalText: '',
|
||||||
isLoading: false
|
isLoading: false
|
||||||
});
|
});
|
||||||
@ -781,6 +786,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false,
|
showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false,
|
||||||
generateEditPlan: openEditPlan,
|
generateEditPlan: openEditPlan,
|
||||||
handleRetryVideo,
|
handleRetryVideo,
|
||||||
isShowAutoEditing: canGoToCut && taskObject.currentStage !== 'final_video' && isGenerateEditPlan && !isShowError ? true : false
|
isShowAutoEditing: canGoToCut && taskObject.currentStage !== 'final_video' && isGenerateEditPlan && !isShowError ? true : false,
|
||||||
|
aspectRatio: state.aspectRatio
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user