From 036488e13b9c069fef3eec94eb59a2641dda901f Mon Sep 17 00:00:00 2001 From: imeepos Date: Sun, 13 Jul 2025 20:49:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=92=8C=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加素材导入对话框组件 - 实现项目详情页面完整功能 - 添加素材状态管理store - 完善TypeScript类型定义 - 更新项目列表路由导航 --- apps/desktop/src/App.tsx | 49 +-- .../src/components/MaterialImportDialog.tsx | 315 ++++++++++++++++++ apps/desktop/src/components/ProjectList.tsx | 6 +- apps/desktop/src/pages/ProjectDetails.tsx | 289 ++++++++++++++++ apps/desktop/src/store/materialStore.ts | 294 ++++++++++++++++ apps/desktop/src/types/material.ts | 261 +++++++++++++++ 6 files changed, 1191 insertions(+), 23 deletions(-) create mode 100644 apps/desktop/src/components/MaterialImportDialog.tsx create mode 100644 apps/desktop/src/pages/ProjectDetails.tsx create mode 100644 apps/desktop/src/store/materialStore.ts create mode 100644 apps/desktop/src/types/material.ts diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index b881824..b71fc70 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,6 +1,8 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { ProjectList } from './components/ProjectList'; import { ProjectForm } from './components/ProjectForm'; +import { ProjectDetails } from './pages/ProjectDetails'; import { useProjectStore } from './store/projectStore'; import { useUIStore } from './store/uiStore'; import { CreateProjectRequest, UpdateProjectRequest } from './types/project'; @@ -45,29 +47,34 @@ function App() { }; return ( -
-
- -
+ +
+
+ + } /> + } /> + +
- {/* 创建项目模态框 */} - {showCreateProjectModal && ( - - )} + {/* 创建项目模态框 */} + {showCreateProjectModal && ( + + )} - {/* 编辑项目模态框 */} - {showEditProjectModal && editingProject && ( - - )} -
+ {/* 编辑项目模态框 */} + {showEditProjectModal && editingProject && ( + + )} +
+ ); } diff --git a/apps/desktop/src/components/MaterialImportDialog.tsx b/apps/desktop/src/components/MaterialImportDialog.tsx new file mode 100644 index 0000000..4fd4725 --- /dev/null +++ b/apps/desktop/src/components/MaterialImportDialog.tsx @@ -0,0 +1,315 @@ +import React, { useState, useEffect } from 'react'; +import { X, Upload, FileText, AlertCircle, CheckCircle, Loader2 } from 'lucide-react'; +import { useMaterialStore } from '../store/materialStore'; +import { CreateMaterialRequest, MaterialImportResult } from '../types/material'; + +interface MaterialImportDialogProps { + isOpen: boolean; + projectId: string; + onClose: () => void; + onImportComplete: (result: MaterialImportResult) => void; +} + +/** + * 素材导入对话框组件 + * 遵循 Tauri 开发规范的组件设计模式 + */ +export const MaterialImportDialog: React.FC = ({ + isOpen, + projectId, + onClose, + onImportComplete +}) => { + const { + isImporting, + importProgress, + error, + importMaterials, + selectMaterialFiles, + validateMaterialFiles, + checkFFmpegAvailable, + clearError + } = useMaterialStore(); + + const [selectedFiles, setSelectedFiles] = useState([]); + const [autoProcess, setAutoProcess] = useState(true); + const [maxSegmentDuration, setMaxSegmentDuration] = useState(300); // 5分钟 + const [ffmpegAvailable, setFFmpegAvailable] = useState(false); + const [step, setStep] = useState<'select' | 'configure' | 'importing' | 'complete'>('select'); + + // 检查 FFmpeg 可用性 + useEffect(() => { + if (isOpen) { + checkFFmpegAvailable().then(setFFmpegAvailable); + } + }, [isOpen, checkFFmpegAvailable]); + + // 重置状态 + useEffect(() => { + if (isOpen) { + setSelectedFiles([]); + setStep('select'); + clearError(); + } + }, [isOpen, clearError]); + + // 选择文件 + const handleSelectFiles = async () => { + try { + const filePaths = await selectMaterialFiles(); + if (filePaths.length > 0) { + const validFiles = await validateMaterialFiles(filePaths); + setSelectedFiles(validFiles); + if (validFiles.length > 0) { + setStep('configure'); + } + } + } catch (error) { + console.error('选择文件失败:', error); + } + }; + + // 开始导入 + const handleStartImport = async () => { + if (selectedFiles.length === 0) return; + + setStep('importing'); + + try { + const request: CreateMaterialRequest = { + project_id: projectId, + file_paths: selectedFiles, + auto_process: autoProcess, + max_segment_duration: maxSegmentDuration, + }; + + const result = await importMaterials(request); + setStep('complete'); + onImportComplete(result); + } catch (error) { + console.error('导入失败:', error); + } + }; + + // 格式化文件大小 + const formatFileSize = (bytes: number) => { + const sizes = ['B', 'KB', 'MB', 'GB']; + if (bytes === 0) return '0 B'; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; + }; + + // 获取文件名 + const getFileName = (path: string) => { + return path.split(/[/\\]/).pop() || path; + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* 头部 */} +
+

导入素材

+ +
+ + {/* 内容 */} +
+ {/* FFmpeg 状态检查 */} + {!ffmpegAvailable && ( +
+
+ + + FFmpeg 不可用,视频处理功能将受限。请确保已安装 FFmpeg。 + +
+
+ )} + + {/* 错误信息 */} + {error && ( +
+
+ + {error} +
+
+ )} + + {/* 步骤 1: 选择文件 */} + {step === 'select' && ( +
+
+ +

选择素材文件

+

+ 支持视频、音频、图片等多种格式 +

+ +
+
+ )} + + {/* 步骤 2: 配置导入选项 */} + {step === 'configure' && ( +
+
+

已选择的文件

+
+ {selectedFiles.map((file, index) => ( +
+ + {getFileName(file)} +
+ ))} +
+
+ +
+

导入配置

+ +
+ setAutoProcess(e.target.checked)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + /> + +
+ + {autoProcess && ( +
+ + setMaxSegmentDuration(Number(e.target.value))} + min="60" + max="3600" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" + /> +

+ 超过此时长的视频将被自动切分 +

+
+ )} +
+
+ )} + + {/* 步骤 3: 导入进度 */} + {step === 'importing' && ( +
+
+ +

正在导入素材

+ + {importProgress && ( +
+
+ {importProgress.current_status} +
+ +
+
+
+ +
+ {importProgress.processed_count} / {importProgress.total_count} 文件已处理 +
+ + {importProgress.current_file && ( +
+ 当前文件: {getFileName(importProgress.current_file)} +
+ )} + + {importProgress.errors.length > 0 && ( +
+

错误信息:

+
+ {importProgress.errors.map((error, index) => ( +
{error}
+ ))} +
+
+ )} +
+ )} +
+
+ )} + + {/* 步骤 4: 完成 */} + {step === 'complete' && ( +
+
+ +

导入完成

+

+ 素材已成功导入到项目中 +

+
+
+ )} +
+ + {/* 底部按钮 */} +
+ {step === 'configure' && ( + <> + + + + )} + + {(step === 'select' || step === 'complete') && ( + + )} +
+
+
+ ); +}; diff --git a/apps/desktop/src/components/ProjectList.tsx b/apps/desktop/src/components/ProjectList.tsx index 4d68f51..92a4674 100644 --- a/apps/desktop/src/components/ProjectList.tsx +++ b/apps/desktop/src/components/ProjectList.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { invoke } from '@tauri-apps/api/core'; import { useProjectStore } from '../store/projectStore'; import { useUIStore } from '../store/uiStore'; @@ -13,6 +14,7 @@ import { Plus, Trash2 } from 'lucide-react'; * 遵循 Tauri 开发规范的组件设计模式 */ export const ProjectList: React.FC = () => { + const navigate = useNavigate(); const { projects, isLoading, @@ -54,8 +56,8 @@ export const ProjectList: React.FC = () => { // 处理项目打开 const handleProjectOpen = (project: any) => { setCurrentProject(project); - // TODO: 导航到项目详情页面 - console.log('打开项目:', project); + // 导航到项目详情页面 + navigate(`/project/${project.id}`); }; // 处理项目编辑 diff --git a/apps/desktop/src/pages/ProjectDetails.tsx b/apps/desktop/src/pages/ProjectDetails.tsx new file mode 100644 index 0000000..0f9fc02 --- /dev/null +++ b/apps/desktop/src/pages/ProjectDetails.tsx @@ -0,0 +1,289 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { ArrowLeft, FolderOpen, Calendar, Settings, Upload } from 'lucide-react'; +import { useProjectStore } from '../store/projectStore'; +import { useMaterialStore } from '../store/materialStore'; +import { Project } from '../types/project'; +import { MaterialImportResult } from '../types/material'; +import { LoadingSpinner } from '../components/LoadingSpinner'; +import { ErrorMessage } from '../components/ErrorMessage'; +import { MaterialImportDialog } from '../components/MaterialImportDialog'; + +/** + * 项目详情页面组件 + * 遵循 Tauri 开发规范的页面组件设计模式 + */ +export const ProjectDetails: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { projects, isLoading, error, loadProjects } = useProjectStore(); + const { + materials, + stats, + loadMaterials, + loadMaterialStats, + isLoading: materialsLoading + } = useMaterialStore(); + const [project, setProject] = useState(null); + const [showImportDialog, setShowImportDialog] = useState(false); + + // 加载项目详情 + useEffect(() => { + if (!projects.length) { + loadProjects(); + } + }, [projects.length, loadProjects]); + + // 根据ID查找项目 + useEffect(() => { + if (id && projects.length > 0) { + const foundProject = projects.find(p => p.id === id); + setProject(foundProject || null); + + // 加载项目素材 + if (foundProject) { + loadMaterials(foundProject.id); + loadMaterialStats(foundProject.id); + } + } + }, [id, projects, loadMaterials, loadMaterialStats]); + + // 返回项目列表 + const handleBack = () => { + navigate('/'); + }; + + // 打开项目文件夹 + const handleOpenFolder = async () => { + if (project) { + try { + const { openPath } = await import('@tauri-apps/plugin-opener'); + await openPath(project.path); + } catch (error) { + console.error('打开文件夹失败:', error); + } + } + }; + + // 素材导入处理 + const handleMaterialImport = () => { + setShowImportDialog(true); + }; + + // 导入完成处理 + const handleImportComplete = (result: MaterialImportResult) => { + setShowImportDialog(false); + console.log('导入完成:', result); + // 重新加载素材列表 + if (project) { + loadMaterials(project.id); + loadMaterialStats(project.id); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ; + } + + if (!project) { + return ( +
+

项目未找到

+

请检查项目ID是否正确

+ +
+ ); + } + + return ( +
+ {/* 页面头部 */} +
+
+ + +
+ + + + + +
+
+ + {/* 项目基本信息 */} +
+
+
+

{project.name}

+ {project.description && ( +

{project.description}

+ )} + +
+
+ + {project.path} +
+
+ + 创建于 {new Date(project.created_at).toLocaleDateString('zh-CN')} +
+
+
+ +
+ {project.is_active ? '活跃' : '非活跃'} +
+
+
+
+ + {/* 项目内容区域 */} +
+ {/* 素材管理 */} +
+
+

素材管理

+ + {/* 素材导入区域 */} +
+ +

导入素材文件

+

+ 支持视频、音频、图片等多种格式,系统将自动分析并处理 +

+ +
+ + {/* 素材列表 */} +
+

项目素材

+ {materialsLoading ? ( +
+ +
+ ) : materials.length > 0 ? ( +
+ {materials.map((material) => ( +
+

{material.name}

+

+ 类型: {material.material_type} +

+

+ 状态: {material.processing_status} +

+

+ 大小: {(material.file_size / 1024 / 1024).toFixed(2)} MB +

+
+ ))} +
+ ) : ( +
+

暂无素材,请导入素材文件开始使用

+
+ )} +
+
+
+ + {/* 项目信息侧边栏 */} +
+ {/* 项目统计 */} +
+

项目统计

+
+
+ 总素材数 + {stats?.total_materials || 0} +
+
+ 视频文件 + {stats?.video_count || 0} +
+
+ 音频文件 + {stats?.audio_count || 0} +
+
+ 图片文件 + {stats?.image_count || 0} +
+
+ 总大小 + + {stats ? (stats.total_size / 1024 / 1024 / 1024).toFixed(2) + ' GB' : '0 GB'} + +
+
+
+ + {/* 最近活动 */} +
+

最近活动

+
+

暂无活动记录

+
+
+
+
+ + {/* 素材导入对话框 */} + {project && ( + setShowImportDialog(false)} + onImportComplete={handleImportComplete} + /> + )} +
+ ); +}; diff --git a/apps/desktop/src/store/materialStore.ts b/apps/desktop/src/store/materialStore.ts new file mode 100644 index 0000000..59a571b --- /dev/null +++ b/apps/desktop/src/store/materialStore.ts @@ -0,0 +1,294 @@ +import { create } from 'zustand'; +import { invoke } from '@tauri-apps/api/core'; +import { + Material, + MaterialStats, + CreateMaterialRequest, + MaterialImportResult, + ProcessingStatus, + MaterialProcessingConfig +} from '../types/material'; + +/** + * 素材状态管理 + * 遵循 Tauri 开发规范的状态管理模式 + */ +interface MaterialState { + // 状态 + materials: Material[]; + currentMaterial: Material | null; + stats: MaterialStats | null; + isLoading: boolean; + isImporting: boolean; + isProcessing: boolean; + error: string | null; + importProgress: { + current_file: string; + processed_count: number; + total_count: number; + current_status: string; + errors: string[]; + } | null; + + // 操作 + loadMaterials: (projectId: string) => Promise; + loadMaterialStats: (projectId: string) => Promise; + importMaterials: (request: CreateMaterialRequest) => Promise; + getMaterialById: (id: string) => Promise; + deleteMaterial: (id: string) => Promise; + processMaterials: (materialIds: string[]) => Promise; + updateMaterialStatus: (id: string, status: ProcessingStatus, errorMessage?: string) => Promise; + cleanupInvalidMaterials: (projectId: string) => Promise; + setCurrentMaterial: (material: Material | null) => void; + clearError: () => void; + + // 工具方法 + selectMaterialFiles: () => Promise; + validateMaterialFiles: (filePaths: string[]) => Promise; + getFileInfo: (filePath: string) => Promise; + checkFFmpegAvailable: () => Promise; + getFFmpegVersion: () => Promise; + getSupportedExtensions: () => Promise; +} + +export const useMaterialStore = create((set, get) => ({ + // 初始状态 + materials: [], + currentMaterial: null, + stats: null, + isLoading: false, + isImporting: false, + isProcessing: false, + error: null, + importProgress: null, + + // 加载项目素材 + loadMaterials: async (projectId: string) => { + set({ isLoading: true, error: null }); + try { + const materials = await invoke('get_project_materials', { projectId }); + set({ materials, isLoading: false }); + } catch (error) { + set({ + error: error as string, + isLoading: false, + materials: [] + }); + } + }, + + // 加载素材统计 + loadMaterialStats: async (projectId: string) => { + try { + const stats = await invoke('get_project_material_stats', { projectId }); + set({ stats }); + } catch (error) { + console.error('加载素材统计失败:', error); + } + }, + + // 导入素材 + importMaterials: async (request: CreateMaterialRequest) => { + set({ isImporting: true, error: null, importProgress: { + current_file: '', + processed_count: 0, + total_count: request.file_paths.length, + current_status: '准备导入...', + errors: [] + }}); + + try { + const result = await invoke('import_materials', { request }); + + // 更新素材列表 + const { materials } = get(); + set({ + materials: [...materials, ...result.created_materials], + isImporting: false, + importProgress: null + }); + + // 重新加载统计信息 + get().loadMaterialStats(request.project_id); + + return result; + } catch (error) { + set({ + error: error as string, + isImporting: false, + importProgress: null + }); + throw error; + } + }, + + // 获取素材详情 + getMaterialById: async (id: string) => { + try { + const material = await invoke('get_material_by_id', { id }); + return material; + } catch (error) { + set({ error: error as string }); + return null; + } + }, + + // 删除素材 + deleteMaterial: async (id: string) => { + try { + await invoke('delete_material', { id }); + + // 从状态中移除 + const { materials } = get(); + set({ materials: materials.filter(m => m.id !== id) }); + + // 如果删除的是当前素材,清空当前素材 + const { currentMaterial } = get(); + if (currentMaterial?.id === id) { + set({ currentMaterial: null }); + } + } catch (error) { + set({ error: error as string }); + throw error; + } + }, + + // 批量处理素材 + processMaterials: async (materialIds: string[]) => { + set({ isProcessing: true, error: null }); + try { + const processedIds = await invoke('batch_process_materials', { materialIds }); + + // 更新处理状态 + const { materials } = get(); + const updatedMaterials = materials.map(material => { + if (processedIds.includes(material.id)) { + return { ...material, processing_status: 'Processing' as ProcessingStatus }; + } + return material; + }); + + set({ materials: updatedMaterials, isProcessing: false }); + } catch (error) { + set({ error: error as string, isProcessing: false }); + throw error; + } + }, + + // 更新素材状态 + updateMaterialStatus: async (id: string, status: ProcessingStatus, errorMessage?: string) => { + try { + await invoke('update_material_status', { id, status, errorMessage }); + + // 更新本地状态 + const { materials } = get(); + const updatedMaterials = materials.map(material => { + if (material.id === id) { + return { + ...material, + processing_status: status, + error_message: errorMessage, + updated_at: new Date().toISOString() + }; + } + return material; + }); + + set({ materials: updatedMaterials }); + } catch (error) { + set({ error: error as string }); + throw error; + } + }, + + // 清理失效素材 + cleanupInvalidMaterials: async (projectId: string) => { + try { + const result = await invoke('cleanup_invalid_materials', { projectId }); + + // 重新加载素材列表 + await get().loadMaterials(projectId); + + return result; + } catch (error) { + set({ error: error as string }); + throw error; + } + }, + + // 设置当前素材 + setCurrentMaterial: (material: Material | null) => { + set({ currentMaterial: material }); + }, + + // 清除错误 + clearError: () => { + set({ error: null }); + }, + + // 选择素材文件 + selectMaterialFiles: async () => { + try { + const filePaths = await invoke('select_material_files'); + return filePaths; + } catch (error) { + set({ error: error as string }); + return []; + } + }, + + // 验证素材文件 + validateMaterialFiles: async (filePaths: string[]) => { + try { + const validFiles = await invoke('validate_material_files', { filePaths }); + return validFiles; + } catch (error) { + set({ error: error as string }); + return []; + } + }, + + // 获取文件信息 + getFileInfo: async (filePath: string) => { + try { + const fileInfo = await invoke('get_file_info', { filePath }); + return fileInfo; + } catch (error) { + set({ error: error as string }); + return null; + } + }, + + // 检查 FFmpeg 是否可用 + checkFFmpegAvailable: async () => { + try { + const isAvailable = await invoke('check_ffmpeg_available'); + return isAvailable; + } catch (error) { + console.error('检查 FFmpeg 失败:', error); + return false; + } + }, + + // 获取 FFmpeg 版本 + getFFmpegVersion: async () => { + try { + const version = await invoke('get_ffmpeg_version'); + return version; + } catch (error) { + console.error('获取 FFmpeg 版本失败:', error); + return 'Unknown'; + } + }, + + // 获取支持的扩展名 + getSupportedExtensions: async () => { + try { + const extensions = await invoke('get_supported_extensions'); + return extensions; + } catch (error) { + console.error('获取支持的扩展名失败:', error); + return []; + } + }, +})); diff --git a/apps/desktop/src/types/material.ts b/apps/desktop/src/types/material.ts new file mode 100644 index 0000000..6310f21 --- /dev/null +++ b/apps/desktop/src/types/material.ts @@ -0,0 +1,261 @@ +/** + * 素材相关类型定义 + * 遵循 Tauri 开发规范的类型安全设计 + */ + +export type MaterialType = 'Video' | 'Audio' | 'Image' | 'Document' | 'Other'; + +export type ProcessingStatus = 'Pending' | 'Processing' | 'Completed' | 'Failed' | 'Skipped'; + +export interface VideoMetadata { + duration: number; + width: number; + height: number; + fps: number; + bitrate: number; + codec: string; + format: string; + has_audio: boolean; + audio_codec?: string; + audio_bitrate?: number; + audio_sample_rate?: number; +} + +export interface AudioMetadata { + duration: number; + bitrate: number; + codec: string; + sample_rate: number; + channels: number; + format: string; +} + +export interface ImageMetadata { + width: number; + height: number; + format: string; + color_space?: string; + has_alpha: boolean; + dpi?: number; +} + +export type MaterialMetadata = + | { Video: VideoMetadata } + | { Audio: AudioMetadata } + | { Image: ImageMetadata } + | 'None'; + +export interface SceneSegment { + start_time: number; + end_time: number; + duration: number; + scene_id: number; + confidence: number; +} + +export interface SceneDetection { + scenes: SceneSegment[]; + total_scenes: number; + detection_method: string; + threshold: number; +} + +export interface MaterialSegment { + id: string; + material_id: string; + segment_index: number; + start_time: number; + end_time: number; + duration: number; + file_path: string; + file_size: number; + created_at: string; +} + +export interface Material { + id: string; + project_id: string; + name: string; + original_path: string; + file_size: number; + md5_hash: string; + material_type: MaterialType; + processing_status: ProcessingStatus; + metadata: MaterialMetadata; + scene_detection?: SceneDetection; + segments: MaterialSegment[]; + created_at: string; + updated_at: string; + processed_at?: string; + error_message?: string; +} + +export interface CreateMaterialRequest { + project_id: string; + file_paths: string[]; + auto_process: boolean; + max_segment_duration?: number; +} + +export interface MaterialProcessingConfig { + max_segment_duration: number; + scene_detection_threshold: number; + enable_scene_detection: boolean; + video_quality: string; + audio_quality: string; + output_format: string; +} + +export interface MaterialImportResult { + total_files: number; + processed_files: number; + skipped_files: number; + failed_files: number; + created_materials: Material[]; + errors: string[]; + processing_time: number; +} + +export interface MaterialStats { + total_materials: number; + video_count: number; + audio_count: number; + image_count: number; + other_count: number; + total_size: number; + total_duration: number; + processing_status_counts: Record; +} + +export interface FileInfo { + name: string; + path: string; + size: number; + extension: string; + material_type: MaterialType; + is_supported: boolean; + modified: number; +} + +/** + * 素材导入相关的 Tauri 命令类型定义 + */ +export interface MaterialCommands { + // 素材管理命令 + import_materials(request: CreateMaterialRequest): Promise; + get_project_materials(project_id: string): Promise; + get_material_by_id(id: string): Promise; + delete_material(id: string): Promise; + get_project_material_stats(project_id: string): Promise; + batch_process_materials(material_ids: string[]): Promise; + update_material_status(id: string, status: string, error_message?: string): Promise; + cleanup_invalid_materials(project_id: string): Promise; + + // 文件操作命令 + is_supported_format(file_path: string): Promise; + get_supported_extensions(): Promise; + select_material_files(): Promise; + validate_material_files(file_paths: string[]): Promise; + get_file_info(file_path: string): Promise; + + // FFmpeg 相关命令 + check_ffmpeg_available(): Promise; + get_ffmpeg_version(): Promise; + extract_file_metadata(file_path: string): Promise; + detect_video_scenes(file_path: string, threshold: number): Promise; + generate_video_thumbnail( + input_path: string, + output_path: string, + timestamp: number, + width: number, + height: number + ): Promise; +} + +/** + * 素材导入进度状态 + */ +export interface ImportProgress { + current_file: string; + processed_count: number; + total_count: number; + current_status: string; + errors: string[]; +} + +/** + * 素材卡片组件属性 + */ +export interface MaterialCardProps { + material: Material; + onView: (material: Material) => void; + onEdit: (material: Material) => void; + onDelete: (id: string) => void; + onProcess: (material: Material) => void; +} + +/** + * 素材导入对话框属性 + */ +export interface MaterialImportDialogProps { + isOpen: boolean; + projectId: string; + onClose: () => void; + onImportComplete: (result: MaterialImportResult) => void; +} + +/** + * 素材列表组件属性 + */ +export interface MaterialListProps { + projectId: string; + materials: Material[]; + isLoading: boolean; + onRefresh: () => void; + onImport: () => void; +} + +/** + * 素材统计组件属性 + */ +export interface MaterialStatsProps { + stats: MaterialStats; + isLoading: boolean; +} + +/** + * 视频预览组件属性 + */ +export interface VideoPreviewProps { + material: Material; + onSegmentSelect?: (segment: MaterialSegment) => void; +} + +/** + * 场景检测结果组件属性 + */ +export interface SceneDetectionProps { + sceneDetection: SceneDetection; + onSceneSelect?: (scene: SceneSegment) => void; +} + +/** + * 素材处理配置组件属性 + */ +export interface ProcessingConfigProps { + config: MaterialProcessingConfig; + onChange: (config: MaterialProcessingConfig) => void; +} + +/** + * 工具函数类型 + */ +export interface MaterialUtils { + formatFileSize(bytes: number): string; + formatDuration(seconds: number): string; + getStatusColor(status: ProcessingStatus): string; + getTypeIcon(type: MaterialType): string; + isVideoType(type: MaterialType): boolean; + isAudioType(type: MaterialType): boolean; + isImageType(type: MaterialType): boolean; +}