+
{/* 左侧图片 */}
- {/* 角色配置区域 */}
- {selectedTemplate?.storyRole &&
- selectedTemplate.storyRole.length > 0 && (
-
-
- Character Configuration
-
-
- {selectedTemplate.storyRole.map((role, index) => (
-
- {/* 图片容器 */}
-
-
- {
- // 更新角色的描述字段
- const updatedTemplate = {
- ...selectedTemplate!,
- storyRole: selectedTemplate!.storyRole.map(
- (r) =>
- r.role_name === role.role_name
- ? {
- ...r,
- role_description: e.target.value,
- }
- : r
- ),
- };
- setSelectedTemplate(updatedTemplate);
- }}
- placeholder={role.user_tips}
- className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
- />
-
- {/* AI生成按钮 */}
-
{
- if (
- role.role_description &&
- role.role_description.trim()
- ) {
- setIsRoleGenerating(prev => ({...prev, [role.role_name]: true}));
- try {
- await handleRoleFieldBlur(
- role.role_name,
- role.role_description.trim()
- );
- } finally {
- setIsRoleGenerating(prev => ({...prev, [role.role_name]: false}));
- }
- }
- setInputVisible((prev) => ({
- ...prev,
- [role.role_name]: false,
- }));
+
+ {/* 角色配置区域 */}
+ {selectedTemplate?.storyRole &&
+ selectedTemplate.storyRole.length > 0 && (
+
+
+ Character Configuration
+
+
+ {selectedTemplate.storyRole.map((role, index) => (
+
- }
- placement="top"
- classNames={{
- root: "max-w-none",
- }}
- open={inputVisible[role.role_name]}
- onOpenChange={(visible) =>
- setInputVisible((prev) => ({
- ...prev,
- [role.role_name]: visible,
- }))
- }
- trigger="contextMenu"
- styles={{ root: { zIndex: 1000 } }}
- >
- {/* 图片 */}
-
+ setInputVisible((prev) => ({
+ ...prev,
+ [role.role_name]: visible,
+ }))
+ }
+ trigger="contextMenu"
+ styles={{ root: { zIndex: 1000 } }}
>
-
-
-
-
- {/* 角色名称 - 图片下方 */}
-
-
- {role.role_name}
-
-
-
- {/* 按钮组 - 右上角 */}
-
- {/* AI生成按钮 */}
-
-
- setInputVisible((prev) => ({
- ...prev,
- [role.role_name]: !prev[role.role_name],
- }))
- }
- className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
+ {/* 图片 */}
+
-
-
+
+
- {/* 上传按钮 */}
-
{
- const isImage = file.type.startsWith("image/");
- if (!isImage) {
- console.error("只能上传图片文件");
- return false;
- }
- const isLt5M = file.size / 1024 / 1024 < 5;
- if (!isLt5M) {
- console.error("图片大小不能超过5MB");
- return false;
- }
- return true;
- }}
- customRequest={async ({
- file,
- onSuccess,
- onError,
- }: RcCustomRequestOptions) => {
- try {
- const fileObj = file as File;
- const uploadedUrl = await uploadFile(
- fileObj,
- (progress: number) => {
- console.log(`上传进度: ${progress}%`);
- }
- );
- await AvatarAndAnalyzeFeatures(
- uploadedUrl,
- role.role_name
- );
- onSuccess?.(uploadedUrl);
- } catch (error) {
- console.error("角色图片上传失败:", error);
- onError?.(error as Error);
- }
- }}
- >
-
+ {/* 角色名称 - 图片下方 */}
+
+
+ {role.role_name}
+
+
+
+ {/* 按钮组 - 右上角 */}
+
+ {/* AI生成按钮 */}
+
+ setInputVisible((prev) => ({
+ ...prev,
+ [role.role_name]: !prev[role.role_name],
+ }))
+ }
+ className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
-
+
-
+
+ {/* 上传按钮 */}
+ {
+ const isImage = file.type.startsWith("image/");
+ if (!isImage) {
+ console.error("只能上传图片文件");
+ return false;
+ }
+ const isLt5M = file.size / 1024 / 1024 < 5;
+ if (!isLt5M) {
+ console.error("图片大小不能超过5MB");
+ return false;
+ }
+ return true;
+ }}
+ customRequest={async ({
+ file,
+ onSuccess,
+ onError,
+ }: RcCustomRequestOptions) => {
+ try {
+ const fileObj = file as File;
+ const uploadedUrl = await uploadFile(
+ fileObj,
+ (progress: number) => {
+ console.log(`上传进度: ${progress}%`);
+ }
+ );
+ await AvatarAndAnalyzeFeatures(
+ uploadedUrl,
+ role.role_name
+ );
+ onSuccess?.(uploadedUrl);
+ } catch (error) {
+ console.error("角色图片上传失败:", error);
+ onError?.(error as Error);
+ }
+ }}
+ >
+
+
+
+
+
+
+
-
- ))}
+ ))}
+
-
- )}
+ )}
- {/* 道具配置区域 */}
- {selectedTemplate?.storyItem &&
- selectedTemplate.storyItem.length > 0 && (
-
+ {/* 道具配置区域 */}
+ {selectedTemplate?.storyItem &&
+ selectedTemplate.storyItem.length > 0 && (
+
+
+ props Configuration
+
+
+ {selectedTemplate.storyItem.map((item, index) => (
+
+ {/* 图片容器 */}
+
+ }
+ placement="top"
+ classNames={{
+ root: "max-w-none",
+ }}
+ open={inputVisible[item.item_name]}
+ onOpenChange={(visible) =>
+ setInputVisible((prev) => ({
+ ...prev,
+ [item.item_name]: visible,
+ }))
+ }
+ trigger="contextMenu"
+ styles={{ root: { zIndex: 1000 } }}
+ >
+ {/* 图片 */}
+
+
+
+
+
+ {/* 道具名称 - 图片下方 */}
+
+
+ {item.item_name}
+
+
+
+ {/* 按钮组 - 右上角 */}
+
+ {/* AI生成按钮 */}
+
+
+ setInputVisible((prev) => ({
+ ...prev,
+ [item.item_name]: !prev[item.item_name],
+ }))
+ }
+ className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
+ >
+
+
+
+
+ {/* 上传按钮 */}
+ {
+ const isImage = file.type.startsWith("image/");
+ if (!isImage) {
+ console.error("只能上传图片文件");
+ return false;
+ }
+ const isLt5M = file.size / 1024 / 1024 < 5;
+ if (!isLt5M) {
+ console.error("图片大小不能超过5MB");
+ return false;
+ }
+ return true;
+ }}
+ customRequest={async ({
+ file,
+ onSuccess,
+ onError,
+ }: RcCustomRequestOptions) => {
+ try {
+ const fileObj = file as File;
+ const uploadedUrl = await uploadFile(
+ fileObj,
+ (progress: number) => {
+ console.log(`上传进度: ${progress}%`);
+ }
+ );
+ updateItemImage(item.item_name, uploadedUrl);
+ onSuccess?.(uploadedUrl);
+ } catch (error) {
+ console.error("道具图片上传失败:", error);
+ onError?.(error as Error);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/** 自由输入文字 */}
+ {freeInputLayout === 'top' && selectedTemplate?.freeInput && selectedTemplate.freeInput.length > 0 && (
+
- props Configuration
+ Input Configuration
-
- {selectedTemplate.storyItem.map((item, index) => (
-
- {/* 图片容器 */}
-
- }
- placement="top"
- classNames={{
- root: "max-w-none",
- }}
- open={inputVisible[item.item_name]}
- onOpenChange={(visible) =>
- setInputVisible((prev) => ({
- ...prev,
- [item.item_name]: visible,
- }))
- }
- trigger="contextMenu"
- styles={{ root: { zIndex: 1000 } }}
- >
- {/* 图片 */}
-
-
-
-
-
- {/* 道具名称 - 图片下方 */}
-
-
- {item.item_name}
-
-
-
- {/* 按钮组 - 右上角 */}
-
- {/* AI生成按钮 */}
-
-
- setInputVisible((prev) => ({
- ...prev,
- [item.item_name]: !prev[item.item_name],
- }))
- }
- className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
- >
-
-
-
-
- {/* 上传按钮 */}
- {
- const isImage = file.type.startsWith("image/");
- if (!isImage) {
- console.error("只能上传图片文件");
- return false;
- }
- const isLt5M = file.size / 1024 / 1024 < 5;
- if (!isLt5M) {
- console.error("图片大小不能超过5MB");
- return false;
- }
- return true;
- }}
- customRequest={async ({
- file,
- onSuccess,
- onError,
- }: RcCustomRequestOptions) => {
- try {
- const fileObj = file as File;
- const uploadedUrl = await uploadFile(
- fileObj,
- (progress: number) => {
- console.log(`上传进度: ${progress}%`);
- }
- );
- updateItemImage(item.item_name, uploadedUrl);
- onSuccess?.(uploadedUrl);
- } catch (error) {
- console.error("道具图片上传失败:", error);
- onError?.(error as Error);
- }
- }}
- >
-
-
-
-
-
-
-
-
-
- ))}
-
+
)}
-
- {/** 自由输入文字 */}
- {freeInputLayout === 'top' && selectedTemplate?.freeInput && selectedTemplate.freeInput.length > 0 && (
-
-
- input Configuration
-
-
- )}
+
{/** 自由输入文字 */}
@@ -813,7 +813,7 @@ export const PcTemplateModal = ({
{templateListRender()}
-
{storyEditorRender()}
+
{storyEditorRender()}
diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx
index 9120912..c54fb43 100644
--- a/components/layout/top-bar.tsx
+++ b/components/layout/top-bar.tsx
@@ -380,7 +380,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
{/* Sign-in entry */}
-
+ {/*
handleSignin()}
@@ -388,7 +388,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe
>
-
+
*/}
{/* AI 积分 */}
diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx
index aed75d9..ff03a77 100644
--- a/components/pages/create-to-video2.tsx
+++ b/components/pages/create-to-video2.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useRef, useCallback } from 'react';
+import type { MouseEvent } from 'react';
import { Loader2, Download } from 'lucide-react';
import { useRouter } from 'next/navigation';
import './style/create-to-video2.css';
@@ -11,7 +12,9 @@ import cover_image1 from '@/public/assets/cover_image3.jpg';
import cover_image2 from '@/public/assets/cover_image_shu.jpg';
import { motion } from 'framer-motion';
import { Tooltip, Button } from 'antd';
-import { downloadVideo, getFirstFrame } from '@/utils/tools';
+import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
+import { showDownloadOptionsModal } from '@/components/pages/work-flow/download-options-modal';
+import { post } from '@/api/request';
import Masonry from 'react-masonry-css';
import debounce from 'lodash/debounce';
@@ -263,6 +266,34 @@ export default function CreateToVideo2() {
}, []);
const renderProjectCard = (project: MovieProject): JSX.Element => {
+ const handleDownloadClick = async (e: MouseEvent, project: MovieProject) => {
+ console.log(project);
+
+ e.stopPropagation();
+ showDownloadOptionsModal({
+ currentVideoIndex: 0,
+ totalVideos: 1,
+ isCurrentVideoFailed: false,
+ isFinalStage: true,
+ projectId: project.project_id,
+ onDownloadCurrent: async (withWatermark: boolean) => {
+ setIsLoadingDownloadBtn(true);
+ try {
+ const json: any = await post('/movie/download_video', {
+ project_id: project.project_id,
+ watermark: withWatermark
+ });
+ const url = json?.data?.download_url as string | undefined;
+ if (url) {
+ await downloadVideo(url);
+ }
+ } finally {
+ setIsLoadingDownloadBtn(false);
+ }
+ },
+ onDownloadAll: ()=>{}
+ });
+ };
// 根据 aspect_ratio 计算纵横比
const getAspectRatio = () => {
switch (project.aspect_ratio) {
@@ -320,12 +351,7 @@ export default function CreateToVideo2() {
{(project.final_video_url || project.final_simple_video_url) && (
- {
- e.stopPropagation(); // 阻止事件冒泡
- setIsLoadingDownloadBtn(true);
- await downloadVideo(project.final_video_url || project.final_simple_video_url);
- setIsLoadingDownloadBtn(false);
- }}>
+ handleDownloadClick(e, project)}>
{isLoadingDownloadBtn ? : }
diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx
index c4a12a2..a64b79b 100644
--- a/components/pages/home-page2.tsx
+++ b/components/pages/home-page2.tsx
@@ -1223,6 +1223,19 @@ function HomeModule5() {
}[]
>(() => {
return plans.map((plan) => {
+ const rawDescription = plan.description ?? '';
+ let creditsText: string = rawDescription;
+ try {
+ const parsed = JSON.parse(rawDescription) as { period: 'month' | 'year'; content: string }[];
+ if (Array.isArray(parsed)) {
+ const match = parsed.find((item) => item && item.period === billingType);
+ if (match && typeof match.content === 'string') {
+ creditsText = match.content;
+ }
+ }
+ } catch {
+ // If not valid JSON, keep original string
+ }
return {
title: plan.display_name || plan.name,
price:
@@ -1232,7 +1245,7 @@ function HomeModule5() {
originalPrice: plan.price_month / 100,
monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 100,
discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`,
- credits: plan.description,
+ credits: creditsText,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
issubscribed: plan.is_subscribed,
features: plan.features || [],
@@ -1302,9 +1315,26 @@ function HomeModule5() {
/* 大屏显示器字体 */
2xl:text-[3.5rem] 2xl:leading-[110%] 2xl:mb-[1.5rem]"
>
- Pick a plan and make it yours
+ CREATE FOR $0.
-
+
+
+ Remove watermark with Credits.
+
{/* 计费切换 */}
-
e.stopPropagation()}
- >
-
-
-
-
-
-
-
-
- Download Options
-
-
-
-
-
- Choose your download preference
-
-
- {!isCurrentVideoFailed && (
-
-
Current video
-
{currentVideoIndex + 1} / {totalVideos}
-
- )}
-
-
-
- {!isCurrentVideoFailed && (
-
{
- onDownloadCurrent();
- onClose();
- }}
- >
-
- {isFinalStage ? 'Download Final Video' : 'Download Current Video'}
-
- )}
-
{
- onDownloadAll();
- onClose();
- }}
- >
-
- Download All Videos ({totalVideos})
-
-
-
-
- );
-}
-
-/**
- * Opens a download options modal with glass morphism style.
- * @param {DownloadOptionsModalProps} options - download options and callbacks.
- */
-function showDownloadOptionsModal(options: Omit
): void {
- if (typeof window === 'undefined' || typeof document === 'undefined') {
- return;
- }
-
- const mount = document.createElement('div');
- mount.setAttribute('data-alt', 'download-options-modal-root');
- document.body.appendChild(mount);
-
- let root: Root | null = null;
- try {
- root = createRoot(mount);
- } catch {
- if (mount.parentNode) {
- mount.parentNode.removeChild(mount);
- }
- return;
- }
-
- const close = () => {
- try {
- root?.unmount();
- } finally {
- if (mount.parentNode) {
- mount.parentNode.removeChild(mount);
- }
- }
- };
-
- root.render(
-
- );
-}
-
/**
* 面向 H5 的媒体预览组件。
* - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。
@@ -221,7 +86,8 @@ export function H5MediaViewer({
const [isPlaying, setIsPlaying] = useState(false);
const [isCatalogOpen, setIsCatalogOpen] = useState(false);
const [isEdgeBrowser, setIsEdgeBrowser] = useState(false);
-
+ const searchParams = useSearchParams();
+ const episodeId = searchParams.get('episodeId') || '';
/** 解析形如 "16:9" 的比例字符串 */
const parseAspect = (input?: string): { w: number; h: number } => {
const parts = (typeof input === 'string' ? input.split(':') : []);
@@ -611,18 +477,19 @@ export function H5MediaViewer({
currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex,
totalVideos,
isCurrentVideoFailed,
- onDownloadCurrent: async () => {
- if (hasUrl) {
- await downloadVideo(current.urls[0]);
- }
- },
- onDownloadAll: async () => {
- const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
- if (hasFinalVideo) {
- all.push(taskObject.final.url);
- }
- await downloadAllVideos(all);
+ projectId: episodeId,
+ videoId: current?.video_id,
+ onDownloadCurrent: async (withWatermark: boolean) => {
+ if (!current?.video_id) return;
+ const json: any = await post('/movie/download_video', {
+ project_id: episodeId,
+ video_id: current.video_id,
+ watermark: withWatermark
+ });
+ const url = json?.data?.download_url as string | undefined;
+ if (url) await downloadVideo(url);
},
+ onDownloadAll: ()=>{}
});
}}
/>
@@ -660,18 +527,16 @@ export function H5MediaViewer({
totalVideos,
isCurrentVideoFailed: false,
isFinalStage: true,
- onDownloadCurrent: async () => {
- if (finalUrl) {
- await downloadVideo(finalUrl);
- }
- },
- onDownloadAll: async () => {
- const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
- if (finalUrl) {
- all.push(finalUrl);
- }
- await downloadAllVideos(all);
+ projectId: episodeId,
+ onDownloadCurrent: async (withWatermark: boolean) => {
+ const json: any = await post('/movie/download_video', {
+ project_id: episodeId,
+ watermark: withWatermark
+ });
+ const url = json?.data?.download_url as string | undefined;
+ if (url) await downloadVideo(url);
},
+ onDownloadAll: ()=>{}
});
}}
/>
diff --git a/components/pages/work-flow/download-options-modal.tsx b/components/pages/work-flow/download-options-modal.tsx
new file mode 100644
index 0000000..e8a0316
--- /dev/null
+++ b/components/pages/work-flow/download-options-modal.tsx
@@ -0,0 +1,182 @@
+'use client';
+
+import React, { useEffect, useRef, useState } from 'react';
+import { Checkbox } from 'antd';
+import { createRoot, Root } from 'react-dom/client';
+import { X, Download, ArrowDownWideNarrow } from 'lucide-react';
+import { post } from '@/api/request';
+
+interface DownloadOptionsModalProps {
+ onDownloadCurrent: (withWatermark: boolean) => void;
+ onDownloadAll: (withWatermark: boolean) => void;
+ onClose: () => void;
+ currentVideoIndex: number;
+ totalVideos: number;
+ /** 当前视频是否生成失败 */
+ isCurrentVideoFailed: boolean;
+ /** 是否为最终视频阶段 */
+ isFinalStage?: boolean;
+ /** 项目ID */
+ projectId?: string;
+ /** 视频ID(分镜视频可用) */
+ videoId?: string;
+}
+
+/**
+ * Download options modal component with glass morphism style.
+ * @param {DownloadOptionsModalProps} props - modal properties.
+ */
+function DownloadOptionsModal(props: DownloadOptionsModalProps) {
+ const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false, projectId, videoId } = props;
+ const containerRef = useRef(null);
+ const [withWatermark, setWithWatermark] = useState(true);
+ const [baseAmount, setBaseAmount] = useState(0);
+ const [isCheckingBalance, setIsCheckingBalance] = useState(false);
+
+ useEffect(() => {
+ const originalOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.body.style.overflow = originalOverflow;
+ };
+ }, []);
+
+ // 监听水印选择变化,请求价格信息
+ useEffect(() => {
+ let aborted = false;
+ const checkBalance = async () => {
+ try {
+ if (!aborted) setIsCheckingBalance(true);
+ const json: any = await post('/movie/download_video', {
+ project_id: projectId,
+ video_id: videoId,
+ watermark: withWatermark,
+ check_balance: true
+ });
+ const amount = json?.data?.base_amount;
+ if (!aborted) setBaseAmount(Number.isFinite(amount) ? Number(amount) : 0);
+ } catch {
+ if (!aborted) setBaseAmount(0);
+ } finally {
+ if (!aborted) setIsCheckingBalance(false);
+ }
+ };
+ void checkBalance();
+ return () => {
+ aborted = true;
+ };
+ }, [withWatermark]);
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+
+
+
+
+
+
+ Download Options
+
+
+
+
+
+ Choose your download preference
+
+
+ {!withWatermark && baseAmount && baseAmount !== 0 ? (
+ -{baseAmount} credits
+ ) : (
+ free
+ )}
+
+
+
+ setWithWatermark(!e.target.checked)}
+ />
+ without watermark
+
+
+
+ {/* stats-info hidden temporarily due to no batch billing support */}
+
+
+
+ {
+ onDownloadCurrent(withWatermark);
+ onClose();
+ }}
+ >
+
+ Download Video
+
+
+
+
+ );
+}
+
+/**
+ * Opens a download options modal with glass morphism style.
+ * @param {DownloadOptionsModalProps} options - download options and callbacks.
+ */
+export function showDownloadOptionsModal(options: Omit): void {
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
+ return;
+ }
+
+ const mount = document.createElement('div');
+ mount.setAttribute('data-alt', 'download-options-modal-root');
+ document.body.appendChild(mount);
+
+ let root: Root | null = null;
+ try {
+ root = createRoot(mount);
+ } catch {
+ if (mount.parentNode) {
+ mount.parentNode.removeChild(mount);
+ }
+ return;
+ }
+
+ const close = () => {
+ try {
+ root?.unmount();
+ } finally {
+ if (mount.parentNode) {
+ mount.parentNode.removeChild(mount);
+ }
+ }
+ };
+
+ root.render(
+
+ );
+}
+
diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx
index fb38313..d6ef89a 100644
--- a/components/pages/work-flow/media-viewer.tsx
+++ b/components/pages/work-flow/media-viewer.tsx
@@ -3,6 +3,7 @@
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, PictureInPicture2, PenTool } from 'lucide-react';
+import { showDownloadOptionsModal } from './download-options-modal';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
@@ -12,9 +13,11 @@ import ScriptLoading from './script-loading';
import { TaskObject } from '@/api/DTO/movieEdit';
import { Button, Tooltip } from 'antd';
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
+import { post } from '@/api/request';
import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
import { EditPoint as EditPointType } from './video-edit/types';
import { isVideoModificationEnabled } from '@/lib/server-config';
+import { useSearchParams } from 'next/navigation';
import error_image from '@/public/assets/error.webp';
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
@@ -89,7 +92,8 @@ export const MediaViewer = React.memo(function MediaViewer({
const [isVideoEditMode, setIsVideoEditMode] = useState(false);
// 控制钢笔图标显示的状态 - 参考谷歌登录按钮的实现
const [showVideoModification, setShowVideoModification] = useState(false);
-
+ const searchParams = useSearchParams();
+ const episodeId = searchParams.get('episodeId') || '';
useEffect(() => {
if (isSmartChatBoxOpen) {
const videoContentWidth = videoContentRef.current?.clientWidth ?? 0;
@@ -505,21 +509,36 @@ export const MediaViewer = React.memo(function MediaViewer({
onClick={() => handleEditClick('3', 'final')}
/>
- {/* 下载所有视频按钮 */}
-
- {
- setIsLoadingDownloadAllVideosBtn(true);
- await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
- setIsLoadingDownloadAllVideosBtn(false);
- }} />
-
{/* 下载按钮 */}
-
- {
- setIsLoadingDownloadBtn(true);
- await downloadVideo(taskObject.final.url);
- setIsLoadingDownloadBtn(false);
- }} />
+
+ {
+ const totalVideos = taskObject.videos.data.filter((video: any) => video.urls && video.urls.length > 0).length;
+ showDownloadOptionsModal({
+ currentVideoIndex: 0,
+ totalVideos: totalVideos + 1,
+ isCurrentVideoFailed: false,
+ isFinalStage: true,
+ projectId: episodeId || '',
+ onDownloadCurrent: async (withWatermark: boolean) => {
+ setIsLoadingDownloadBtn(true);
+ try {
+ const json: any = await post('/movie/download_video', {
+ project_id: episodeId,
+ watermark: withWatermark
+ });
+ const url = json?.data?.download_url as string | undefined;
+ if (url) await downloadVideo(url);
+ } finally {
+ setIsLoadingDownloadBtn(false);
+ }
+ },
+ onDownloadAll: () => {}
+ });
+ }}
+ />
{showGotoCutButton && (
@@ -648,15 +667,41 @@ export const MediaViewer = React.memo(function MediaViewer({
)}
-
- {
- const currentVideo = taskObject.videos.data[currentSketchIndex];
- if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0) {
- setIsLoadingDownloadBtn(true);
- await downloadVideo(currentVideo.urls[0]);
- setIsLoadingDownloadBtn(false);
- }
- }} />
+
+ {
+ const currentVideo = taskObject.videos.data[currentSketchIndex];
+ const totalVideos = taskObject.videos.data.filter((video: any) => video.urls && video.urls.length > 0).length;
+ const isCurrentVideoFailed = currentVideo.video_status === 2;
+
+ showDownloadOptionsModal({
+ currentVideoIndex: currentSketchIndex,
+ totalVideos: taskObject.final.url ? totalVideos + 1 : totalVideos,
+ isCurrentVideoFailed: isCurrentVideoFailed,
+ isFinalStage: false,
+ projectId: episodeId,
+ videoId: currentVideo?.video_id,
+ onDownloadCurrent: async (withWatermark: boolean) => {
+ if (!currentVideo?.video_id) return;
+ setIsLoadingDownloadBtn(true);
+ try {
+ const json: any = await post('/movie/download_video', {
+ project_id: episodeId,
+ video_id: currentVideo.video_id,
+ watermark: withWatermark
+ });
+ const url = json?.data?.download_url as string | undefined;
+ if (url) await downloadVideo(url);
+ } finally {
+ setIsLoadingDownloadBtn(false);
+ }
+ },
+ onDownloadAll: () => {}
+ });
+ }}
+ />
>
) : (
@@ -681,14 +726,6 @@ export const MediaViewer = React.memo(function MediaViewer({
}
}} />
- {/* 下载所有视频按钮 */}
-
- {
- setIsLoadingDownloadAllVideosBtn(true);
- await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
- setIsLoadingDownloadAllVideosBtn(false);
- }} />
-
{/* 跳转剪辑按钮 */}
{showGotoCutButton && (
diff --git a/components/script-renderer/ScriptRenderer.tsx b/components/script-renderer/ScriptRenderer.tsx
index 17c6766..7eaef95 100644
--- a/components/script-renderer/ScriptRenderer.tsx
+++ b/components/script-renderer/ScriptRenderer.tsx
@@ -177,13 +177,13 @@ export const ScriptRenderer: React.FC = ({ data, setIsPause
{addThemeTag.map((item, index) => (
{item}
-
+ {/*
handleThemeTagChange(addThemeTag.filter(v => v !== item))
- } />
+ } /> */}
))}
{/* 主题标签更改 */}
-
+ {/*
= ({ data, setIsPause
}}
/>
-
+
*/}
)
default:
@@ -212,7 +212,7 @@ export const ScriptRenderer: React.FC
= ({ data, setIsPause
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
- {
// 提示权限不够
@@ -220,7 +220,7 @@ export const ScriptRenderer: React.FC = ({ data, setIsPause
return;
handleEditBlock(block);
}}
- />
+ /> */}
{