import React, { useState, useEffect, useCallback } from 'react'; import { Image, FolderOpen, Play, Settings, Clock, List, Grid, RefreshCw, Download, Eye, Trash2, CheckCircle, AlertCircle, Info } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { open } from '@tauri-apps/plugin-dialog'; import { FrameExtractionConfig, VideoFileInfo, FrameExtractionResult, FrameType, ImageFormat, ExtractionTaskStatus, DEFAULT_FRAME_EXTRACTION_CONFIG, SUPPORTED_VIDEO_FORMATS, FRAME_EXTRACTION_PRESETS } from '../../types/frame-extractor'; import { FrameExtractionConfigPanel } from '../../components/frame-extractor/FrameExtractionConfigPanel'; import { VideoFileList } from '../../components/frame-extractor/VideoFileList'; import { ExtractionProgress } from '../../components/frame-extractor/ExtractionProgress'; import { FramePreview } from '../../components/frame-extractor/FramePreview'; import { ExtractionResults } from '../../components/frame-extractor/ExtractionResults'; import { VideoPlayer } from '../../components/frame-extractor/VideoPlayer'; /** * 视频关键帧提取工具主组件 * 遵循 Tauri 开发规范和 UI/UX 设计标准 */ const FrameExtractorTool: React.FC = () => { // 状态管理 const [selectedVideos, setSelectedVideos] = useState([]); const [config, setConfig] = useState(DEFAULT_FRAME_EXTRACTION_CONFIG); const [isExtracting, setIsExtracting] = useState(false); const [isScanning, setIsScanning] = useState(false); const [extractionResults, setExtractionResults] = useState([]); const [currentProgress, setCurrentProgress] = useState(0); const [currentFile, setCurrentFile] = useState(''); const [viewMode, setViewMode] = useState<'config' | 'progress' | 'results'>('config'); const [selectedFolder, setSelectedFolder] = useState(''); const [ffmpegAvailable, setFfmpegAvailable] = useState(null); const [selectedVideoForPreview, setSelectedVideoForPreview] = useState(null); const [currentPlayTime, setCurrentPlayTime] = useState(0); const [recursiveScan, setRecursiveScan] = useState(true); // 检查 FFmpeg 可用性 useEffect(() => { checkFFmpegAvailability(); }, []); const checkFFmpegAvailability = useCallback(async () => { try { const available = await invoke('check_ffmpeg_availability'); setFfmpegAvailable(available); if (!available) { console.warn('FFmpeg 不可用,某些功能可能无法正常工作'); } } catch (error) { console.error('检查 FFmpeg 可用性失败:', error); setFfmpegAvailable(false); } }, []); // 选择单个视频文件 const handleSelectFiles = useCallback(async () => { try { const selected = await open({ multiple: true, filters: [{ name: '视频文件', extensions: SUPPORTED_VIDEO_FORMATS }], title: '选择视频文件', }); if (selected && Array.isArray(selected)) { await loadVideoFiles(selected); } else if (selected && typeof selected === 'string') { await loadVideoFiles([selected]); } } catch (error) { console.error('选择文件失败:', error); } }, []); // 选择文件夹 const handleSelectFolder = useCallback(async () => { try { const selected = await open({ directory: true, multiple: false, title: '选择视频文件夹', }); if (selected && typeof selected === 'string') { setSelectedFolder(selected); await scanVideoFiles(selected, recursiveScan); } } catch (error) { console.error('选择文件夹失败:', error); } }, []); // 扫描视频文件 const scanVideoFiles = useCallback(async (folderPath: string, recursive: boolean) => { setIsScanning(true); setCurrentFile(`正在扫描文件夹: ${folderPath}${recursive ? ' (递归)' : ''}`); try { const files = await invoke('scan_video_files', { request: { folder_path: folderPath, recursive, supported_formats: SUPPORTED_VIDEO_FORMATS, } }); const validFiles = files.filter(f => f.is_valid); setSelectedVideos(validFiles); // 显示扫描结果 const totalFiles = files.length; const validCount = validFiles.length; const invalidCount = totalFiles - validCount; console.log(`扫描完成: 找到 ${totalFiles} 个文件,其中 ${validCount} 个有效,${invalidCount} 个无效`); setCurrentFile(`扫描完成: 找到 ${validCount} 个有效视频文件`); // 2秒后清除状态信息 setTimeout(() => { setCurrentFile(''); }, 2000); } catch (error) { console.error('扫描视频文件失败:', error); setCurrentFile('扫描失败,请重试'); setTimeout(() => { setCurrentFile(''); }, 3000); } finally { setIsScanning(false); } }, []); // 加载视频文件信息 const loadVideoFiles = useCallback(async (filePaths: string[]) => { setIsScanning(true); const videoInfos: VideoFileInfo[] = []; try { for (const filePath of filePaths) { try { const info = await invoke('get_video_info', { videoPath: filePath }); videoInfos.push(info); } catch (error) { console.error(`获取视频信息失败 ${filePath}:`, error); } } setSelectedVideos(videoInfos.filter(info => info.is_valid)); } finally { setIsScanning(false); } }, []); // 开始提取 const handleStartExtraction = useCallback(async () => { if (selectedVideos.length === 0) { alert('请先选择视频文件'); return; } if (!config.output_directory) { alert('请设置输出目录'); return; } setIsExtracting(true); setViewMode('progress'); setCurrentProgress(0); setExtractionResults([]); try { const videoPaths = selectedVideos.map(v => v.path); const results = await invoke('extract_frames_batch', { request: { video_files: videoPaths, config: config, } }); setExtractionResults(results); setViewMode('results'); const successCount = results.filter(r => r.success).length; console.log(`提取完成: ${successCount}/${results.length} 成功`); } catch (error) { console.error('批量提取失败:', error); alert(`提取失败: ${error}`); } finally { setIsExtracting(false); } }, [selectedVideos, config]); // 清除选择 const handleClearSelection = useCallback(() => { setSelectedVideos([]); setSelectedFolder(''); setExtractionResults([]); setCurrentProgress(0); setCurrentFile(''); }, []); // 应用预设配置 const handleApplyPreset = useCallback((presetId: string) => { const preset = FRAME_EXTRACTION_PRESETS.find(p => p.id === presetId); if (preset) { setConfig({ ...preset.config }); } }, []); return (
{/* 页面标题 */}

视频关键帧提取工具

快速提取视频的第一帧、最后帧或自定义位置的帧

{/* FFmpeg 状态指示器 */}
{ffmpegAvailable === null ? (
检查中...
) : ffmpegAvailable ? (
FFmpeg 可用
) : (
FFmpeg 不可用
)}
{/* 工具栏 */}
{/* 递归扫描选项 */}
已选择 {selectedVideos.length} 个视频文件
{/* 状态显示区域 */} {(isScanning || currentFile) && (
{isScanning && } {currentFile || '正在处理...'}
)} {/* 视图切换 */}
{/* 主要内容区域 */}
{/* 左侧:视频文件列表 */}
{ const newVideos = [...selectedVideos]; newVideos.splice(index, 1); setSelectedVideos(newVideos); // 如果删除的是当前预览的视频,清除预览 if (selectedVideoForPreview && selectedVideos[index]?.path === selectedVideoForPreview.path) { setSelectedVideoForPreview(null); } }} onPreviewVideo={(video: VideoFileInfo) => { setSelectedVideoForPreview(video); setViewMode('config'); // 切换到配置视图以显示预览 }} />
{/* 右侧:主要内容 */}
{viewMode === 'config' && ( <> {/* 视频预览播放器 */} {selectedVideoForPreview && (

视频预览

)} {/* 配置面板 */} )} {viewMode === 'progress' && ( )} {viewMode === 'results' && ( { // 打开结果文件所在目录 invoke('open_file_directory', { path: result.output_path }); }} /> )}
); }; export default FrameExtractorTool;