From 1132cb9777ae168f0b57887bbf24d49b8ac1dffb Mon Sep 17 00:00:00 2001 From: root Date: Thu, 10 Jul 2025 13:33:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20AI=20=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E7=94=9F=E6=88=90=E7=BB=93=E6=9E=9C=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎬 新增功能: 1. AI 视频结果预览组件 (AIVideoResultPreview): - 支持单个和批量视频生成结果预览 - 内置视频播放器,支持在线播放 - 显示生成提示词、本地路径、在线链接 - 批量结果支持视频选择器浏览多个视频 - 优雅的错误处理和加载状态 2. 视频预览功能: - HTML5 视频播放器,支持完整控制 - 自动使用原图作为视频封面 - 播放失败时显示友好错误信息 - 响应式设计,适配不同屏幕尺寸 3. 批量结果管理: - 显示批量处理统计信息(成功/失败数量) - 视频选择器,方便浏览多个生成结果 - 每个视频显示对应的提示词和路径信息 - 统一的操作界面和交互体验 4. 文件系统集成: - 新增 open_folder Tauri 命令 - 跨平台支持(Windows/macOS/Linux) - 一键打开视频所在文件夹 - 备用方案:显示完整文件路径 5. 用户界面增强: - 任务完成后显示 '预览视频' 按钮 - 改进的成功状态显示,包含统计信息 - 模态对话框形式的预览界面 - 直观的操作按钮和状态指示 6. 操作功能: - 打开文件夹:直接定位到视频文件位置 - 在新窗口打开:使用浏览器播放在线视频 - 复制路径:方便用户获取文件信息 - 关闭预览:返回主界面 ✅ 用户体验提升: - 生成完成后可立即预览结果 ✓ - 批量处理结果一目了然 ✓ - 便捷的文件管理操作 ✓ - 专业的视频播放体验 ✓ 现在用户可以直接在应用内预览生成的 AI 视频! --- src-tauri/src/commands.rs | 44 ++++ src-tauri/src/lib.rs | 3 +- src/components/AIVideoGenerator.tsx | 71 ++++++- src/components/AIVideoResultPreview.tsx | 260 ++++++++++++++++++++++++ 4 files changed, 368 insertions(+), 10 deletions(-) create mode 100644 src/components/AIVideoResultPreview.tsx diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2ff5358..21778f4 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -514,3 +514,47 @@ pub async fn select_folder(app: tauri::AppHandle) -> Result { } } } + +#[tauri::command] +pub async fn open_folder(folder_path: String) -> Result { + println!("Opening folder: {}", folder_path); + + #[cfg(target_os = "windows")] + { + use std::process::Command; + let result = Command::new("explorer") + .arg(&folder_path) + .spawn(); + + match result { + Ok(_) => Ok(format!("Opened folder: {}", folder_path)), + Err(e) => Err(format!("Failed to open folder: {}", e)) + } + } + + #[cfg(target_os = "macos")] + { + use std::process::Command; + let result = Command::new("open") + .arg(&folder_path) + .spawn(); + + match result { + Ok(_) => Ok(format!("Opened folder: {}", folder_path)), + Err(e) => Err(format!("Failed to open folder: {}", e)) + } + } + + #[cfg(target_os = "linux")] + { + use std::process::Command; + let result = Command::new("xdg-open") + .arg(&folder_path) + .spawn(); + + match result { + Ok(_) => Ok(format!("Opened folder: {}", folder_path)), + Err(e) => Err(format!("Failed to open folder: {}", e)) + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7f2ca03..27822a7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,7 +33,8 @@ pub fn run() { commands::batch_generate_ai_videos, commands::test_ai_video_environment, commands::select_image_file, - commands::select_folder + commands::select_folder, + commands::open_folder ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/AIVideoGenerator.tsx b/src/components/AIVideoGenerator.tsx index 067ce63..8c3e376 100644 --- a/src/components/AIVideoGenerator.tsx +++ b/src/components/AIVideoGenerator.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react' import { Upload, Play, Folder, FileText, Clock, Cpu, Trash2, Download } from 'lucide-react' import { useAIVideoStore, useAIVideoJobs, useAIVideoProcessing, useAIVideoSettings } from '../stores/useAIVideoStore' import { invoke } from '@tauri-apps/api/core' +import AIVideoResultPreview from './AIVideoResultPreview' interface AIVideoGeneratorProps { className?: string @@ -17,7 +18,18 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = const [outputFolder, setOutputFolder] = useState('') const [duration, setDuration] = useState('5') const [modelType, setModelType] = useState('lite') - + const [previewData, setPreviewData] = useState<{ + isOpen: boolean + result: any + jobType: 'single' | 'batch' + prompt?: string + imagePath?: string + }>({ + isOpen: false, + result: null, + jobType: 'single' + }) + // Store const { generateSingleVideo, @@ -38,6 +50,25 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = setModelType(defaultModelType) }, [defaultDuration, defaultModelType]) + // Preview functions + const openPreview = (job: any) => { + setPreviewData({ + isOpen: true, + result: job.result, + jobType: job.type, + prompt: job.type === 'single' ? job.request.prompt : undefined, + imagePath: job.type === 'single' ? job.request.imagePath : undefined + }) + } + + const closePreview = () => { + setPreviewData({ + isOpen: false, + result: null, + jobType: 'single' + }) + } + // Handle file selection const handleImageSelect = async () => { try { @@ -414,14 +445,26 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = )} {job.result && job.status === 'completed' && ( -
- 生成成功! - {job.result.video_path && ( - - )} +
+
+
+
✅ 生成成功!
+ {job.type === 'batch' && job.result.success_count && ( +
+ 成功: {job.result.success_count} 个,失败: {job.result.failed_count || 0} 个 +
+ )} +
+ {(job.result.video_path || job.result.video_url || job.result.results) && ( + + )} +
)}
@@ -429,6 +472,16 @@ const AIVideoGenerator: React.FC = ({ className = '' }) = )} + + {/* Video Preview Modal */} + ) } diff --git a/src/components/AIVideoResultPreview.tsx b/src/components/AIVideoResultPreview.tsx new file mode 100644 index 0000000..50a9cb5 --- /dev/null +++ b/src/components/AIVideoResultPreview.tsx @@ -0,0 +1,260 @@ +import React, { useState } from 'react' +import { X, Download, ExternalLink, Play, Folder } from 'lucide-react' +import { invoke } from '@tauri-apps/api/core' + +interface VideoResult { + status?: boolean + video_path?: string + video_url?: string + msg?: string +} + +interface BatchResult { + status: boolean + success_count: number + failed_count: number + results: Array<{ + image_path: string + prompt: string + result: VideoResult + }> + msg: string +} + +interface AIVideoResultPreviewProps { + isOpen: boolean + onClose: () => void + result: VideoResult | BatchResult + jobType: 'single' | 'batch' + prompt?: string + imagePath?: string +} + +const AIVideoResultPreview: React.FC = ({ + isOpen, + onClose, + result, + jobType, + prompt, + imagePath +}) => { + const [selectedVideoIndex, setSelectedVideoIndex] = useState(0) + const [videoError, setVideoError] = useState(false) + + if (!isOpen) return null + + const handleVideoError = () => setVideoError(true) + + const handleOpenFolder = async (videoPath: string) => { + try { + // Extract folder path + const folderPath = videoPath.substring(0, videoPath.lastIndexOf('\\') || videoPath.lastIndexOf('/')) + console.log('Opening folder:', folderPath) + + // Try to open folder using Tauri command + try { + await invoke('open_folder', { folderPath }) + console.log('Folder opened successfully') + } catch (tauriError) { + console.error('Failed to open folder via Tauri:', tauriError) + // Fallback to showing path + alert(`视频已保存到: ${videoPath}\n\n文件夹: ${folderPath}`) + } + } catch (error) { + console.error('Failed to open folder:', error) + alert(`无法打开文件夹: ${error}`) + } + } + + const handleOpenUrl = (url: string) => { + window.open(url, '_blank') + } + + // Render single video result + const renderSingleVideo = (videoResult: VideoResult, videoPrompt?: string, videoImagePath?: string) => ( +
+ {/* Video Player */} +
+ {videoError ? ( +
+
+
视频加载失败
+
+ 请尝试下载到本地查看 +
+
+
+ ) : ( + + )} +
+ + {/* Video Info */} +
+ {videoPrompt && ( +
+ +
+ {videoPrompt} +
+
+ )} + + {videoResult.video_path && ( +
+ +
+ {videoResult.video_path} +
+
+ )} + + {videoResult.msg && ( +
+ +
+ {videoResult.msg} +
+
+ )} +
+ + {/* Actions */} +
+ {videoResult.video_path && ( + + )} + + {videoResult.video_url && ( + + )} +
+
+ ) + + // Render batch results + const renderBatchResults = (batchResult: BatchResult) => { + const successResults = batchResult.results.filter(r => r.result.status) + + if (successResults.length === 0) { + return ( +
+
没有成功生成的视频
+
+ ) + } + + const currentResult = successResults[selectedVideoIndex] + + return ( +
+ {/* Batch Summary */} +
+
批量处理结果
+
+ 总共处理: {batchResult.results.length} 个 | + 成功: {batchResult.success_count} 个 | + 失败: {batchResult.failed_count} 个 +
+
+ + {/* Video Selector */} + {successResults.length > 1 && ( +
+ +
+ {successResults.map((_, index) => ( + + ))} +
+
+ )} + + {/* Current Video */} + {currentResult && renderSingleVideo( + currentResult.result, + currentResult.prompt, + currentResult.image_path + )} +
+ ) + } + + return ( +
+
+ {/* Header */} +
+

+ {jobType === 'batch' ? '批量生成结果' : '视频生成结果'} +

+ +
+ + {/* Content */} +
+ {jobType === 'batch' ? + renderBatchResults(result as BatchResult) : + renderSingleVideo(result as VideoResult, prompt, imagePath) + } + + {/* Close Button */} +
+ +
+
+
+
+ ) +} + +export default AIVideoResultPreview