Spaces:
Build error
Build error
| import { useState, useRef, useCallback } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { useDropzone } from 'react-dropzone' | |
| import { | |
| FaImage, FaVideo, FaMusic, FaCamera, FaUpload, FaDownload, | |
| FaPlay, FaPause, FaStop, FaRedo, FaCut, FaPalette, FaMagic, | |
| FaTrash, FaCopy, FaSave, FaShare, FaExpand, FaCompress, FaFilter, | |
| FaAdjust, FaSun, FaMoon, FaEraser, FaPaintBrush, FaCrop, FaRotate | |
| } from 'react-icons/fa' | |
| import Webcam from 'react-webcam' | |
| import toast from 'react-hot-toast' | |
| export default function MediaStudio({ language, theme }) { | |
| const [activeTab, setActiveTab] = useState('image') | |
| const [selectedFile, setSelectedFile] = useState(null) | |
| const [isProcessing, setIsProcessing] = useState(false) | |
| const [filters, setFilters] = useState({ | |
| brightness: 100, | |
| contrast: 100, | |
| saturation: 100, | |
| blur: 0, | |
| hue: 0 | |
| }) | |
| const [history, setHistory] = useState([]) | |
| const [historyIndex, setHistoryIndex] = useState(-1) | |
| const [cameraActive, setCameraActive] = useState(false) | |
| const [recording, setRecording] = useState(false) | |
| const webcamRef = useRef(null) | |
| const canvasRef = useRef(null) | |
| const tabs = [ | |
| { id: 'image', name: language === 'fa' ? 'تصویر' : 'Image', icon: FaImage }, | |
| { id: 'video', name: language === 'fa' ? 'ویدیو' : 'Video', icon: FaVideo }, | |
| { id: 'audio', name: language === 'fa' ? 'صدا' : 'Audio', icon: FaMusic }, | |
| { id: 'camera', name: language === 'fa' ? 'دوربین' : 'Camera', icon: FaCamera } | |
| ] | |
| const onDrop = useCallback((acceptedFiles) => { | |
| const file = acceptedFiles[0] | |
| if (file) { | |
| const reader = new FileReader() | |
| reader.onload = (e) => { | |
| setSelectedFile({ | |
| name: file.name, | |
| type: file.type, | |
| url: e.target.result, | |
| size: file.size | |
| }) | |
| saveToHistory(e.target.result) | |
| toast.success(language === 'fa' ? 'فایل با موفقیت آپلود شد' : 'File uploaded successfully') | |
| } | |
| reader.readAsDataURL(file) | |
| } | |
| }, []) | |
| const { getRootProps, getInputProps, isDragActive } = useDropzone({ | |
| onDrop, | |
| accept: { | |
| 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'], | |
| 'video/*': ['.mp4', '.avi', '.mov', '.webm'], | |
| 'audio/*': ['.mp3', '.wav', '.ogg', '.m4a'] | |
| } | |
| }) | |
| const saveToHistory = (data) => { | |
| const newHistory = history.slice(0, historyIndex + 1) | |
| newHistory.push(data) | |
| setHistory(newHistory) | |
| setHistoryIndex(newHistory.length - 1) | |
| } | |
| const applyFilter = (filterType, value) => { | |
| setFilters(prev => ({ ...prev, [filterType]: value })) | |
| // Apply filter to canvas | |
| if (selectedFile && canvasRef.current) { | |
| const canvas = canvasRef.current | |
| const ctx = canvas.getContext('2d') | |
| const img = new Image() | |
| img.onload = () => { | |
| canvas.width = img.width | |
| canvas.height = img.height | |
| ctx.filter = `brightness(${filters.brightness}%) contrast(${filters.contrast}%) saturate(${filters.saturation}%) blur(${filters.blur}px) hue-rotate(${filters.hue}deg)` | |
| ctx.drawImage(img, 0, 0) | |
| } | |
| img.src = selectedFile.url | |
| } | |
| } | |
| const capturePhoto = () => { | |
| if (webcamRef.current) { | |
| const imageSrc = webcamRef.current.getScreenshot() | |
| setSelectedFile({ | |
| name: `capture_${Date.now()}.jpg`, | |
| type: 'image/jpeg', | |
| url: imageSrc, | |
| size: 0 | |
| }) | |
| saveToHistory(imageSrc) | |
| toast.success(language === 'fa' ? 'عکس گرفته شد' : 'Photo captured') | |
| } | |
| } | |
| const startRecording = () => { | |
| setRecording(true) | |
| toast.success(language === 'fa' ? 'ضبط شروع شد' : 'Recording started') | |
| } | |
| const stopRecording = () => { | |
| setRecording(false) | |
| toast.success(language === 'fa' ? 'ضبط متوقف شد' : 'Recording stopped') | |
| } | |
| const resetFilters = () => { | |
| setFilters({ | |
| brightness: 100, | |
| contrast: 100, | |
| saturation: 100, | |
| blur: 0, | |
| hue: 0 | |
| }) | |
| } | |
| const downloadFile = () => { | |
| if (selectedFile) { | |
| const a = document.createElement('a') | |
| a.href = selectedFile.url | |
| a.download = `edited_${selectedFile.name}` | |
| a.click() | |
| toast.success(language === 'fa' ? 'فایل دانلود شد' : 'File downloaded') | |
| } | |
| } | |
| const undo = () => { | |
| if (historyIndex > 0) { | |
| setHistoryIndex(historyIndex - 1) | |
| setSelectedFile(prev => ({ ...prev, url: history[historyIndex - 1] })) | |
| } | |
| } | |
| const redo = () => { | |
| if (historyIndex < history.length - 1) { | |
| setHistoryIndex(historyIndex + 1) | |
| setSelectedFile(prev => ({ ...prev, url: history[historyIndex + 1] })) | |
| } | |
| } | |
| return ( | |
| <div className="flex flex-col h-full space-y-4"> | |
| {/* Tab Navigation */} | |
| <div className="flex space-x-reverse space-x-1 p-2 bg-gray-800 rounded-lg"> | |
| {tabs.map(tab => { | |
| const Icon = tab.icon | |
| return ( | |
| <button | |
| key={tab.id} | |
| onClick={() => setActiveTab(tab.id)} | |
| className={`flex items-center space-x-reverse space-x-2 px-4 py-2 rounded-lg transition-all ${ | |
| activeTab === tab.id | |
| ? 'bg-primary-600 text-white' | |
| : 'bg-gray-700 text-gray-300 hover:bg-gray-600' | |
| }`} | |
| > | |
| <Icon className="w-4 h-4" /> | |
| <span className="text-sm font-medium">{tab.name}</span> | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| {/* Main Content */} | |
| <div className="flex-1 flex space-x-reverse space-x-4 min-h-0"> | |
| {/* Media Preview Area */} | |
| <div className="flex-1 bg-gray-800 rounded-lg p-4"> | |
| {activeTab === 'camera' ? ( | |
| <div className="h-full flex flex-col items-center justify-center"> | |
| {cameraActive ? ( | |
| <div className="relative"> | |
| <Webcam | |
| audio={false} | |
| ref={webcamRef} | |
| screenshotFormat="image/jpeg" | |
| className="rounded-lg" | |
| /> | |
| <div className="absolute bottom-4 left-1/2 transform translate-x-1/2 flex space-x-reverse space-x-2"> | |
| <button | |
| onClick={capturePhoto} | |
| className="p-3 bg-primary-600 text-white rounded-full hover:bg-primary-700 transition-all" | |
| > | |
| <FaCamera className="w-5 h-5" /> | |
| </button> | |
| {recording ? ( | |
| <button | |
| onClick={stopRecording} | |
| className="p-3 bg-red-600 text-white rounded-full hover:bg-red-700 transition-all animate-pulse" | |
| > | |
| <FaStop className="w-5 h-5" /> | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={startRecording} | |
| className="p-3 bg-red-600 text-white rounded-full hover:bg-red-700 transition-all" | |
| > | |
| <FaPlay className="w-5 h-5" /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ) : ( | |
| <button | |
| onClick={() => setCameraActive(true)} | |
| className="flex flex-col items-center space-y-2 p-8 bg-gray-700 rounded-lg hover:bg-gray-600 transition-all" | |
| > | |
| <FaCamera className="w-12 h-12 text-gray-400" /> | |
| <span className="text-gray-300"> | |
| {language === 'fa' ? 'فعال کردن دوربین' : 'Enable Camera'} | |
| </span> | |
| </button> | |
| )} | |
| </div> | |
| ) : selectedFile ? ( | |
| <div className="h-full flex flex-col"> | |
| <div className="flex-1 flex items-center justify-center"> | |
| {selectedFile.type.startsWith('image/') ? ( | |
| <img | |
| src={selectedFile.url} | |
| alt={selectedFile.name} | |
| className="max-w-full max-h-full object-contain rounded-lg" | |
| /> | |
| ) : selectedFile.type.startsWith('video/') ? ( | |
| <video | |
| src={selectedFile.url} | |
| controls | |
| className="max-w-full max-h-full rounded-lg" | |
| /> | |
| ) : ( | |
| <audio | |
| src={selectedFile.url} | |
| controls | |
| className="w-full" | |
| /> | |
| )} | |
| </div> | |
| <canvas ref={canvasRef} className="hidden" /> | |
| </div> | |
| ) : ( | |
| <div | |
| {...getRootProps()} | |
| className="h-full flex flex-col items-center justify-center border-2 border-dashed border-gray-600 rounded-lg cursor-pointer hover:border-primary-500 transition-all" | |
| > | |
| <input {...getInputProps()} /> | |
| <FaUpload className="w-12 h-12 text-gray-400 mb-4" /> | |
| <p className="text-gray-300 text-center"> | |
| {isDragActive | |
| ? (language === 'fa' ? 'فایل را رها کنید' : 'Drop the file here') | |
| : (language === 'fa' ? 'فایل را بکشید یا کلیک کنید' : 'Drag & drop or click to upload') | |
| } | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Tools Panel */} | |
| <div className="w-80 bg-gray-800 rounded-lg p-4 space-y-4 overflow-y-auto"> | |
| {/* File Info */} | |
| {selectedFile && ( | |
| <div className="p-3 bg-gray-700 rounded-lg"> | |
| <h3 className="text-sm font-medium mb-2"> | |
| {language === 'fa' ? 'اطلاعات فایل' : 'File Info'} | |
| </h3> | |
| <div className="space-y-1 text-xs text-gray-300"> | |
| <p><span className="font-medium">{language === 'fa' ? 'نام:' : 'Name:'}</span> {selectedFile.name}</p> | |
| <p><span className="font-medium">{language === 'fa' ? 'نوع:' : 'Type:'}</span> {selectedFile.type}</p> | |
| <p><span className="font-medium">{language === 'fa' ? 'حجم:' : 'Size:'}</span> {(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p> | |
| </div> | |
| </div> | |
| )} | |
| {/* Actions */} | |
| <div className="space-y-2"> | |
| <h3 className="text-sm font-medium"> | |
| {language === 'fa' ? 'عملیات' : 'Actions'} | |
| </h3> | |
| <div className="grid grid-cols-2 gap-2"> | |
| <button | |
| onClick={downloadFile} | |
| disabled={!selectedFile} | |
| className="flex items-center justify-center space-x-reverse space-x-1 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-all" | |
| > | |
| <FaDownload className="w-3 h-3" /> | |
| <span className="text-xs">{language === 'fa' ? 'دانلود' : 'Download'}</span> | |
| </button> | |
| <button | |
| onClick={undo} | |
| disabled={historyIndex <= 0} | |
| className="flex items-center justify-center space-x-reverse space-x-1 px-3 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 disabled:opacity-50 transition-all" | |
| > | |
| <FaRedo className="w-3 h-3" /> | |
| <span className="text-xs">{language === 'fa' ? 'بازگشت' : 'Undo'}</span> | |
| </button> | |
| <button | |
| onClick={redo} | |
| disabled={historyIndex >= history.length - 1} | |
| className="flex items-center justify-center space-x-reverse space-x-1 px-3 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 disabled:opacity-50 transition-all" | |
| > | |
| <FaRedo className="w-3 h-3" /> | |
| <span className="text-xs">{language === 'fa' ? 'انجام مجدد' : 'Redo'}</span> | |
| </button> | |
| <button | |
| onClick={resetFilters} | |
| disabled={!selectedFile} | |
| className="flex items-center justify-center space-x-reverse space-x-1 px-3 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-all" | |
| > | |
| <FaEraser className="w-3 h-3" /> | |
| <span className="text-xs">{language === 'fa' ? 'بازنشانی' : 'Reset'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Filters (for images) */} | |
| {selectedFile && selectedFile.type.startsWith('image/') && ( | |
| <div className="space-y-3"> | |
| <h3 className="text-sm font-medium"> | |
| {language === 'fa' ? 'فیلترها' : 'Filters'} | |
| </h3> | |
| <div className="space-y-3"> | |
| <div> | |
| <label className="text-xs text-gray-400 flex items-center justify-between mb-1"> | |
| <span>{language === 'fa' ? 'روشنایی' : 'Brightness'}</span> | |
| <span>{filters.brightness}%</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="200" | |
| value={filters.brightness} | |
| onChange={(e) => applyFilter('brightness', e.target.value)} | |
| className="w-full" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-400 flex items-center justify-between mb-1"> | |
| <span>{language === 'fa' ? 'کنتراست' : 'Contrast'}</span> | |
| <span>{filters.contrast}%</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="200" | |
| value={filters.contrast} | |
| onChange={(e) => applyFilter('contrast', e.target.value)} | |
| className="w-full" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-400 flex items-center justify-between mb-1"> | |
| <span>{language === 'fa' ? 'اشباع' : 'Saturation'}</span> | |
| <span>{filters.saturation}%</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="200" | |
| value={filters.saturation} | |
| onChange={(e) => applyFilter('saturation', e.target.value)} | |
| className="w-full" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-400 flex items-center justify-between mb-1"> | |
| <span>{language === 'fa' ? 'تاری' : 'Blur'}</span> | |
| <span>{filters.blur}px</span> | |
| </label> | |
| <input | |
| type="range" | |
| min="0" | |
| max="10" | |
| value={filters.blur} | |
| onChange={(e) => applyFilter('blur', e.target.value)} | |
| className="w-full" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-xs text-gray-400 flex items-center justify-between mb-1"> | |
| <span>{language === 'fa' |