forked from 77media/video-flow
更新 使用新的下载接口,更新pricing相关的展示
This commit is contained in:
parent
c38b6ad483
commit
153d0d49f8
@ -77,6 +77,19 @@ function HomeModule5() {
|
|||||||
}[]
|
}[]
|
||||||
>(() => {
|
>(() => {
|
||||||
return plans.map((plan) => {
|
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 {
|
return {
|
||||||
title: plan.display_name || plan.name,
|
title: plan.display_name || plan.name,
|
||||||
price:
|
price:
|
||||||
@ -86,7 +99,7 @@ function HomeModule5() {
|
|||||||
originalPrice: plan.price_month / 100,
|
originalPrice: plan.price_month / 100,
|
||||||
monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 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!`,
|
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",
|
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
|
||||||
issubscribed: plan.is_subscribed,
|
issubscribed: plan.is_subscribed,
|
||||||
features: plan.features || [],
|
features: plan.features || [],
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
import { Loader2, Download } from 'lucide-react';
|
import { Loader2, Download } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import './style/create-to-video2.css';
|
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 cover_image2 from '@/public/assets/cover_image_shu.jpg';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Tooltip, Button } from 'antd';
|
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 Masonry from 'react-masonry-css';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
@ -263,6 +266,34 @@ export default function CreateToVideo2() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderProjectCard = (project: MovieProject): JSX.Element => {
|
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 计算纵横比
|
// 根据 aspect_ratio 计算纵横比
|
||||||
const getAspectRatio = () => {
|
const getAspectRatio = () => {
|
||||||
switch (project.aspect_ratio) {
|
switch (project.aspect_ratio) {
|
||||||
@ -320,12 +351,7 @@ export default function CreateToVideo2() {
|
|||||||
{(project.final_video_url || project.final_simple_video_url) && (
|
{(project.final_video_url || project.final_simple_video_url) && (
|
||||||
<div className="absolute top-1 right-1">
|
<div className="absolute top-1 right-1">
|
||||||
<Tooltip placement="top" title="Download">
|
<Tooltip placement="top" title="Download">
|
||||||
<Button size="small" type="text" disabled={isLoadingDownloadBtn} 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" onClick={async (e) => {
|
<Button size="small" type="text" disabled={isLoadingDownloadBtn} 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" onClick={(e) => handleDownloadClick(e, project)}>
|
||||||
e.stopPropagation(); // 阻止事件冒泡
|
|
||||||
setIsLoadingDownloadBtn(true);
|
|
||||||
await downloadVideo(project.final_video_url || project.final_simple_video_url);
|
|
||||||
setIsLoadingDownloadBtn(false);
|
|
||||||
}}>
|
|
||||||
{isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />}
|
{isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -1223,6 +1223,19 @@ function HomeModule5() {
|
|||||||
}[]
|
}[]
|
||||||
>(() => {
|
>(() => {
|
||||||
return plans.map((plan) => {
|
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 {
|
return {
|
||||||
title: plan.display_name || plan.name,
|
title: plan.display_name || plan.name,
|
||||||
price:
|
price:
|
||||||
@ -1232,7 +1245,7 @@ function HomeModule5() {
|
|||||||
originalPrice: plan.price_month / 100,
|
originalPrice: plan.price_month / 100,
|
||||||
monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 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!`,
|
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",
|
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
|
||||||
issubscribed: plan.is_subscribed,
|
issubscribed: plan.is_subscribed,
|
||||||
features: plan.features || [],
|
features: plan.features || [],
|
||||||
|
|||||||
@ -181,6 +181,7 @@ const UsageView: React.FC = () => {
|
|||||||
const formatSource = useCallback((source: string | undefined) => {
|
const formatSource = useCallback((source: string | undefined) => {
|
||||||
if (!source) return '-';
|
if (!source) return '-';
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
|
video_download: 'Video Download',
|
||||||
video_generation: 'Video Generation',
|
video_generation: 'Video Generation',
|
||||||
manual_admin: 'Manual (Admin)',
|
manual_admin: 'Manual (Admin)',
|
||||||
subscription: 'Subscription',
|
subscription: 'Subscription',
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
|||||||
import ScriptLoading from './script-loading';
|
import ScriptLoading from './script-loading';
|
||||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||||
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||||
|
import { post } from '@/api/request';
|
||||||
import { Drawer } from 'antd';
|
import { Drawer } from 'antd';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import error_image from '@/public/assets/error.webp';
|
import error_image from '@/public/assets/error.webp';
|
||||||
import { showDownloadOptionsModal } from './download-options-modal';
|
import { showDownloadOptionsModal } from './download-options-modal';
|
||||||
|
|
||||||
@ -84,7 +86,8 @@ export function H5MediaViewer({
|
|||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
|
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
|
||||||
const [isEdgeBrowser, setIsEdgeBrowser] = useState<boolean>(false);
|
const [isEdgeBrowser, setIsEdgeBrowser] = useState<boolean>(false);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const episodeId = searchParams.get('episodeId') || '';
|
||||||
/** 解析形如 "16:9" 的比例字符串 */
|
/** 解析形如 "16:9" 的比例字符串 */
|
||||||
const parseAspect = (input?: string): { w: number; h: number } => {
|
const parseAspect = (input?: string): { w: number; h: number } => {
|
||||||
const parts = (typeof input === 'string' ? input.split(':') : []);
|
const parts = (typeof input === 'string' ? input.split(':') : []);
|
||||||
@ -474,18 +477,19 @@ export function H5MediaViewer({
|
|||||||
currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex,
|
currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex,
|
||||||
totalVideos,
|
totalVideos,
|
||||||
isCurrentVideoFailed,
|
isCurrentVideoFailed,
|
||||||
onDownloadCurrent: async () => {
|
projectId: episodeId,
|
||||||
if (hasUrl) {
|
videoId: current?.video_id,
|
||||||
await downloadVideo(current.urls[0]);
|
onDownloadCurrent: async (withWatermark: boolean) => {
|
||||||
}
|
if (!current?.video_id) return;
|
||||||
},
|
const json: any = await post('/movie/download_video', {
|
||||||
onDownloadAll: async () => {
|
project_id: episodeId,
|
||||||
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
|
video_id: current.video_id,
|
||||||
if (hasFinalVideo) {
|
watermark: !withWatermark
|
||||||
all.push(taskObject.final.url);
|
});
|
||||||
}
|
const url = json?.data?.download_url as string | undefined;
|
||||||
await downloadAllVideos(all);
|
if (url) await downloadVideo(url);
|
||||||
},
|
},
|
||||||
|
onDownloadAll: ()=>{}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -523,18 +527,16 @@ export function H5MediaViewer({
|
|||||||
totalVideos,
|
totalVideos,
|
||||||
isCurrentVideoFailed: false,
|
isCurrentVideoFailed: false,
|
||||||
isFinalStage: true,
|
isFinalStage: true,
|
||||||
onDownloadCurrent: async () => {
|
projectId: episodeId,
|
||||||
if (finalUrl) {
|
onDownloadCurrent: async (withWatermark: boolean) => {
|
||||||
await downloadVideo(finalUrl);
|
const json: any = await post('/movie/download_video', {
|
||||||
}
|
project_id: episodeId,
|
||||||
},
|
watermark: !withWatermark
|
||||||
onDownloadAll: async () => {
|
});
|
||||||
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
|
const url = json?.data?.download_url as string | undefined;
|
||||||
if (finalUrl) {
|
if (url) await downloadVideo(url);
|
||||||
all.push(finalUrl);
|
|
||||||
}
|
|
||||||
await downloadAllVideos(all);
|
|
||||||
},
|
},
|
||||||
|
onDownloadAll: ()=>{}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Checkbox } from 'antd';
|
||||||
import { createRoot, Root } from 'react-dom/client';
|
import { createRoot, Root } from 'react-dom/client';
|
||||||
import { X, Download, ArrowDownWideNarrow } from 'lucide-react';
|
import { X, Download, ArrowDownWideNarrow } from 'lucide-react';
|
||||||
|
import { baseUrl } from '@/lib/env';
|
||||||
|
|
||||||
interface DownloadOptionsModalProps {
|
interface DownloadOptionsModalProps {
|
||||||
onDownloadCurrent: () => void;
|
onDownloadCurrent: (withWatermark: boolean) => void;
|
||||||
onDownloadAll: () => void;
|
onDownloadAll: (withWatermark: boolean) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
currentVideoIndex: number;
|
currentVideoIndex: number;
|
||||||
totalVideos: number;
|
totalVideos: number;
|
||||||
@ -14,6 +16,10 @@ interface DownloadOptionsModalProps {
|
|||||||
isCurrentVideoFailed: boolean;
|
isCurrentVideoFailed: boolean;
|
||||||
/** 是否为最终视频阶段 */
|
/** 是否为最终视频阶段 */
|
||||||
isFinalStage?: boolean;
|
isFinalStage?: boolean;
|
||||||
|
/** 项目ID */
|
||||||
|
projectId?: string;
|
||||||
|
/** 视频ID(分镜视频可用) */
|
||||||
|
videoId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,8 +27,10 @@ interface DownloadOptionsModalProps {
|
|||||||
* @param {DownloadOptionsModalProps} props - modal properties.
|
* @param {DownloadOptionsModalProps} props - modal properties.
|
||||||
*/
|
*/
|
||||||
function DownloadOptionsModal(props: DownloadOptionsModalProps) {
|
function DownloadOptionsModal(props: DownloadOptionsModalProps) {
|
||||||
const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false } = props;
|
const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false, projectId, videoId } = props;
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [withWatermark, setWithWatermark] = useState<boolean>(true);
|
||||||
|
const [baseAmount, setBaseAmount] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const originalOverflow = document.body.style.overflow;
|
const originalOverflow = document.body.style.overflow;
|
||||||
@ -32,6 +40,46 @@ function DownloadOptionsModal(props: DownloadOptionsModalProps) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 监听水印选择变化,请求价格信息
|
||||||
|
useEffect(() => {
|
||||||
|
let aborted = false;
|
||||||
|
const checkBalance = async () => {
|
||||||
|
try {
|
||||||
|
if (!projectId) {
|
||||||
|
setBaseAmount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = typeof window !== 'undefined' ? (localStorage?.getItem('token') || '') : '';
|
||||||
|
const res = await fetch(`${baseUrl}/movie/download_video`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_id: projectId,
|
||||||
|
video_id: videoId,
|
||||||
|
watermark: !withWatermark,
|
||||||
|
check_balance: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
if (!aborted) setBaseAmount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = await res.json().catch(() => null);
|
||||||
|
const amount = json?.data?.base_amount;
|
||||||
|
if (!aborted) setBaseAmount(Number.isFinite(amount) ? Number(amount) : 0);
|
||||||
|
} catch {
|
||||||
|
if (!aborted) setBaseAmount(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void checkBalance();
|
||||||
|
return () => {
|
||||||
|
aborted = true;
|
||||||
|
};
|
||||||
|
}, [withWatermark]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@ -67,39 +115,38 @@ function DownloadOptionsModal(props: DownloadOptionsModalProps) {
|
|||||||
<p data-alt="modal-description" className="text-sm text-white/80 text-center">
|
<p data-alt="modal-description" className="text-sm text-white/80 text-center">
|
||||||
Choose your download preference
|
Choose your download preference
|
||||||
</p>
|
</p>
|
||||||
|
<div data-alt="price-indicator" className="mt-2 text-center text-sm font-medium">
|
||||||
{!isCurrentVideoFailed && (
|
{baseAmount && baseAmount !== 0 ? (
|
||||||
<div data-alt="stats-info" className="mt-3 rounded-lg bg-white/5 border border-white/10 p-3 text-center">
|
<span className="text-red-400">-{baseAmount} credits</span>
|
||||||
<div className="text-xs text-white/60">Current video</div>
|
) : (
|
||||||
<div className="text-sm font-medium">{currentVideoIndex + 1} / {totalVideos}</div>
|
<span className="text-green-400">free</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div data-alt="watermark-select" className="mt-3 flex items-center justify-center">
|
||||||
|
<div data-alt="watermark-toggle" className="inline-flex items-center gap-2 text-sm text-white/90">
|
||||||
|
<Checkbox
|
||||||
|
data-alt="watermark-checkbox"
|
||||||
|
checked={!withWatermark}
|
||||||
|
onChange={(e) => setWithWatermark(!e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>without watermark</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
{/* stats-info hidden temporarily due to no batch billing support */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-alt="modal-actions" className="mt-6 space-y-3">
|
<div data-alt="modal-actions" className="mt-6 space-y-3">
|
||||||
{!isCurrentVideoFailed && (
|
|
||||||
<button
|
|
||||||
data-alt="download-current-button"
|
|
||||||
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-600/80 to-purple-700/80 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
onDownloadCurrent();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Download size={16} />
|
|
||||||
{isFinalStage ? 'Download Final Video' : 'Download Current Video'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
data-alt="download-all-button"
|
data-alt="download-single-button"
|
||||||
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-500/60 to-purple-600/60 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2 border border-purple-400/30"
|
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-600/80 to-purple-700/80 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onDownloadAll();
|
onDownloadCurrent(withWatermark);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDownWideNarrow size={16} />
|
<Download size={16} />
|
||||||
Download All Videos ({totalVideos})
|
Download Video
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user