From 4cdd6560fc571f95b4e564bffd70f83d90edb94b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 10 Jul 2025 10:00:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20MixVideo=20V2=20?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=9E=B6=E6=9E=84=E5=92=8C=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎬 主要功能: - ✅ 完整的 Tauri + React + Python 分层架构 - ✅ 视频处理核心模块(剪切、调整、特效) - ✅ 音频处理集成(节拍检测、频谱分析) - ✅ Tauri-Python 桥接通信 - ✅ 项目管理和文件管理服务 - ✅ 现代化 UI 组件(时间轴、预览、媒体库) 🛠️ 技术栈: - Frontend: Tauri 2.6.2 + React 18 + TypeScript + Tailwind CSS - Backend: Python 3.10 + MoviePy + FFmpeg + Librosa + OpenCV - State: Zustand stores for project and media management - Build: Vite + pnpm 📦 新增组件: - VideoPreview: 视频预览和播放控制 - Timeline: 多轨道时间轴编辑器 - MediaLibrary: 媒体文件管理和拖拽上传 - TauriService: 前端与 Python 通信服务 - ProjectStore/MediaStore: 状态管理 🧪 已测试功能: - Python 视频处理模块正常工作 - 项目创建和管理功能 - 前端构建成功 - Tauri 应用启动正常 --- src/components/MediaLibrary.tsx | 310 +++++++++++++++++ src/components/Timeline.tsx | 228 +++++++++++++ src/components/VideoPreview.tsx | 279 +++++++++++++++ src/pages/EditorPage.tsx | 111 ++---- src/pages/HomePage.tsx | 70 +++- src/services/tauri.ts | 358 ++++++++++++++++++++ src/stores/useMediaStore.ts | 578 ++++++++++++++++++++++++++++++++ src/stores/useProjectStore.ts | 264 +++++++++++++++ 8 files changed, 2113 insertions(+), 85 deletions(-) create mode 100644 src/components/MediaLibrary.tsx create mode 100644 src/components/Timeline.tsx create mode 100644 src/components/VideoPreview.tsx create mode 100644 src/services/tauri.ts create mode 100644 src/stores/useMediaStore.ts create mode 100644 src/stores/useProjectStore.ts diff --git a/src/components/MediaLibrary.tsx b/src/components/MediaLibrary.tsx new file mode 100644 index 0000000..ea9736b --- /dev/null +++ b/src/components/MediaLibrary.tsx @@ -0,0 +1,310 @@ +import React, { useState, useRef } from 'react' +import { Upload, Video, Music, Image, File, Search, Filter, Grid, List } from 'lucide-react' +import { useMediaStore } from '../stores/useMediaStore' +import { useProjectStore } from '../stores/useProjectStore' + +interface MediaItem { + id: string + name: string + type: 'video' | 'audio' | 'image' + path: string + size: number + duration?: number + thumbnail?: string + createdAt: string +} + +interface MediaLibraryProps { + className?: string +} + +const MediaLibrary: React.FC = ({ className = '' }) => { + const fileInputRef = useRef(null) + const [searchTerm, setSearchTerm] = useState('') + const [filterType, setFilterType] = useState<'all' | 'video' | 'audio' | 'image'>('all') + const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') + const [dragOver, setDragOver] = useState(false) + + const { setCurrentMedia } = useMediaStore() + const { currentProject, addVideoTrack, addAudioTrack } = useProjectStore() + + // Mock media items - in real app this would come from the project + const [mediaItems, setMediaItems] = useState([ + { + id: '1', + name: 'sample_video.mp4', + type: 'video', + path: '/tmp/test_video.mp4', + size: 1024000, + duration: 30, + createdAt: '2025-07-10T09:00:00Z' + }, + { + id: '2', + name: 'background_music.mp3', + type: 'audio', + path: '/tmp/audio.mp3', + size: 512000, + duration: 120, + createdAt: '2025-07-10T09:15:00Z' + } + ]) + + // Filter media items + const filteredItems = mediaItems.filter(item => { + const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) + const matchesFilter = filterType === 'all' || item.type === filterType + return matchesSearch && matchesFilter + }) + + // Handle file upload + const handleFileUpload = (files: FileList | null) => { + if (!files) return + + Array.from(files).forEach(file => { + const fileType = getFileType(file) + if (fileType) { + const newItem: MediaItem = { + id: crypto.randomUUID(), + name: file.name, + type: fileType, + path: URL.createObjectURL(file), + size: file.size, + createdAt: new Date().toISOString() + } + + setMediaItems(prev => [...prev, newItem]) + } + }) + } + + // Determine file type + const getFileType = (file: File): 'video' | 'audio' | 'image' | null => { + if (file.type.startsWith('video/')) return 'video' + if (file.type.startsWith('audio/')) return 'audio' + if (file.type.startsWith('image/')) return 'image' + return null + } + + // Handle drag and drop + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + setDragOver(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + setDragOver(false) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragOver(false) + handleFileUpload(e.dataTransfer.files) + } + + // Handle media item click + const handleMediaClick = (item: MediaItem) => { + setCurrentMedia(item.path) + } + + // Handle adding to timeline + const handleAddToTimeline = (item: MediaItem) => { + if (!currentProject) return + + if (item.type === 'video') { + addVideoTrack({ + id: crypto.randomUUID(), + name: item.name, + file_path: item.path, + start_time: 0, + duration: item.duration || 0 + }) + } else if (item.type === 'audio') { + addAudioTrack({ + id: crypto.randomUUID(), + name: item.name, + file_path: item.path, + start_time: 0, + duration: item.duration || 0, + volume: 1.0 + }) + } + } + + // Format file size + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + // Format duration + const formatDuration = (seconds?: number): string => { + if (!seconds) return '' + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}:${secs.toString().padStart(2, '0')}` + } + + // Get icon for media type + const getMediaIcon = (type: string) => { + switch (type) { + case 'video': return