- 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
89 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|