forge/frontend/components/ImageCropper.tsx
DJP d7852fc399 feat: Complete Veo support and UI enhancements
- Backend:
  - Implemented robust Veo video generation including 'lastFrame' and 'referenceImages' support
  - Fixed video URI extraction with recursive search for API response stability
  - Implemented direct HTTP video download to resolve SDK method missing errors
- Frontend (Video Generator):
  - Updated validation to allow Text-to-Video for Veo without requiring a first frame
  - Fixed job state clearing to prevent UI from showing previous completion status
- Frontend (My Files & Library):
  - Moved batch actions toolbar to bottom-left to prevent blocking pagination
  - Added 'Deselect All' button to batch actions toolbar
  - Added file type indicators to asset cards
- Components:
  - Added 'Clear Finished' button to Active Jobs tracker
  - Updated Asset Library modal toolbar positioning
2025-12-11 15:43:55 -05:00

89 lines
3.4 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import Cropper from 'react-easy-crop';
import { X, Check, Loader2 } from 'lucide-react';
import getCroppedImg from '@/lib/imageUtils';
interface ImageCropperProps {
image: string;
aspect: number;
onCrop: (croppedBlob: Blob) => void;
onCancel: () => void;
}
export default function ImageCropper({ image, aspect, onCrop, onCancel }: ImageCropperProps) {
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
const [loading, setLoading] = useState(false);
// Define onCropComplete as a useCallback to avoid re-renders
const onCropComplete = useCallback((croppedArea: any, croppedAreaPixels: any) => {
setCroppedAreaPixels(croppedAreaPixels);
}, []);
const handleSave = async () => {
if (!croppedAreaPixels) return;
setLoading(true);
try {
const croppedImage = await getCroppedImg(image, croppedAreaPixels);
onCrop(croppedImage);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 z-[100] bg-black/90 flex flex-col animate-in fade-in duration-200">
<div className="p-4 flex items-center justify-between border-b border-gray-800 bg-forge-dark z-10">
<h3 className="text-white font-medium">Crop Image</h3>
<button
onClick={handleSave}
disabled={loading}
className="px-4 py-1.5 bg-forge-yellow text-black rounded-lg font-medium flex items-center gap-2 hover:bg-yellow-400 disabled:opacity-50"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Apply Crop
</button>
</div>
<div className="flex-1 relative bg-black">
<Cropper
image={image}
crop={crop}
zoom={zoom}
aspect={aspect}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
classes={{
containerClassName: "bg-black",
mediaClassName: "",
cropAreaClassName: "border-2 border-forge-yellow"
}}
/>
</div>
<div className="p-4 bg-forge-dark border-t border-gray-800 z-10 flex items-center gap-4">
<button onClick={onCancel} className="text-gray-400 hover:text-white font-medium">
Cancel
</button>
<div className="flex-1 flex items-center gap-4">
<span className="text-gray-400 text-center text-sm w-12">Zoom</span>
<input
type="range"
value={zoom}
min={1}
max={3}
step={0.1}
onChange={(e) => setZoom(Number(e.target.value))}
className="flex-1 accent-forge-yellow h-1 bg-gray-700 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
</div>
);
}