fix: modal actually processes image with rotation/crop and fixes zoom
Critical fixes: 1. Modal now applies rotation and crop to image, returns processed blob 2. Frontend sends processed image to backend (not original) 3. Zoom slider min/max now calculated based on image size 4. Initial zoom shows entire image in canvas 5. Zoom constraints prevent over-zooming or under-zooming User experience improved: - Sees full item at start (auto-fit zoom) - Can adjust rotation/crop smoothly - Adjusted image is what gets saved (not original) - Zoom slider works correctly for any image size Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,13 +60,12 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
||||
setShowImageAdjustment(true);
|
||||
};
|
||||
|
||||
const handleImageAdjustmentConfirm = async (adjustments: { rotation: number; cropBounds: { x: number; y: number; width: number; height: number } } | null) => {
|
||||
const handleImageAdjustmentConfirm = async (adjustments: any) => {
|
||||
if (!pendingItemData) return;
|
||||
|
||||
const { index } = pendingItemData;
|
||||
|
||||
if (adjustments && extractedItems[index]) {
|
||||
// Override AI-detected values with user adjustments
|
||||
const updatedItems = [...extractedItems];
|
||||
updatedItems[index] = {
|
||||
...updatedItems[index],
|
||||
@@ -82,11 +81,27 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
||||
user_adjusted: true
|
||||
}
|
||||
};
|
||||
setExtractedItems(updatedItems);
|
||||
|
||||
// If modal processed and returned image blob, use that instead of original
|
||||
if (adjustments.imageBlob) {
|
||||
// Convert blob to base64 for extracted_image_bytes
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = (reader.result as string).split(',')[1];
|
||||
updatedItems[index].extractedImageBlob = adjustments.imageBlob;
|
||||
updatedItems[index]._base64ImageData = base64;
|
||||
setExtractedItems(updatedItems);
|
||||
hookConfirmSingleItem(index);
|
||||
};
|
||||
reader.readAsDataURL(adjustments.imageBlob);
|
||||
} else {
|
||||
setExtractedItems(updatedItems);
|
||||
hookConfirmSingleItem(index);
|
||||
}
|
||||
} else {
|
||||
hookConfirmSingleItem(index);
|
||||
}
|
||||
|
||||
// Now call the hook's confirm with the updated data
|
||||
hookConfirmSingleItem(index);
|
||||
setShowImageAdjustment(false);
|
||||
setAdjustingItemIndex(null);
|
||||
setPendingItemData(null);
|
||||
|
||||
@@ -5,13 +5,15 @@ import { X, RotateCw } from 'lucide-react';
|
||||
|
||||
interface ImageAdjustmentModalProps {
|
||||
imageUrl: string;
|
||||
onConfirm: (adjustedImage: { rotation: number; cropBounds: { x: number; y: number; width: number; height: number } } | null) => void;
|
||||
onConfirm: (adjustedImage: { rotation: number; cropBounds: { x: number; y: number; width: number; height: number }; imageBlob?: Blob } | null) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdjustmentModalProps) {
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [minZoom, setMinZoom] = useState(0.1);
|
||||
const [maxZoom, setMaxZoom] = useState(5);
|
||||
const [panX, setPanX] = useState(0);
|
||||
const [panY, setPanY] = useState(0);
|
||||
const [useImage, setUseImage] = useState(true);
|
||||
@@ -51,8 +53,9 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
||||
const canvasHeight = 600;
|
||||
const zoomX = canvasWidth / img.width;
|
||||
const zoomY = canvasHeight / img.height;
|
||||
const fitZoom = Math.min(zoomX, zoomY, 1); // Don't zoom in, only out to fit
|
||||
setZoom(fitZoom);
|
||||
const fitZoom = Math.min(zoomX, zoomY); // Allow any zoom needed to fit
|
||||
setMinZoom(fitZoom); // Min zoom shows entire image
|
||||
setZoom(fitZoom); // Start at fit level
|
||||
};
|
||||
img.src = imageUrl;
|
||||
}, [imageUrl]);
|
||||
@@ -115,18 +118,45 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (useImage && cropBounds) {
|
||||
onConfirm({ rotation, cropBounds });
|
||||
} else {
|
||||
const handleConfirm = async () => {
|
||||
if (!useImage || !cropBounds || !imageRef.current) {
|
||||
onConfirm(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process image: apply rotation and crop
|
||||
const processCanvas = document.createElement('canvas');
|
||||
const ctx = processCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const { x, y, width, height } = cropBounds;
|
||||
|
||||
// Set canvas to crop dimensions
|
||||
processCanvas.width = width;
|
||||
processCanvas.height = height;
|
||||
|
||||
// Apply rotation around crop center and crop
|
||||
ctx.save();
|
||||
ctx.translate(width / 2, height / 2);
|
||||
ctx.rotate((rotation * Math.PI) / 180);
|
||||
ctx.translate(-imageDimensions.width / 2 + x, -imageDimensions.height / 2 + y);
|
||||
ctx.drawImage(imageRef.current, 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
// Convert to blob
|
||||
processCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
onConfirm({ rotation, cropBounds, imageBlob: blob });
|
||||
} else {
|
||||
onConfirm({ rotation, cropBounds });
|
||||
}
|
||||
}, 'image/jpeg', 0.95);
|
||||
};
|
||||
|
||||
const handleWheel = (e: React.WheelEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
setZoom(prev => Math.max(0.5, Math.min(5, prev * delta)));
|
||||
setZoom(prev => Math.max(minZoom, Math.min(maxZoom, prev * delta)));
|
||||
};
|
||||
|
||||
const getTouchDistance = (touches: React.TouchList) => {
|
||||
@@ -154,7 +184,7 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
||||
const newDistance = getTouchDistance(e.touches);
|
||||
if (lastTouchDistance > 0) {
|
||||
const delta = newDistance / lastTouchDistance;
|
||||
setZoom(prev => Math.max(0.5, Math.min(5, prev * delta)));
|
||||
setZoom(prev => Math.max(minZoom, Math.min(maxZoom, prev * delta)));
|
||||
setLastTouchDistance(newDistance);
|
||||
}
|
||||
} else if (e.touches.length === 1 && isPanning) {
|
||||
@@ -213,8 +243,8 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="5"
|
||||
min={minZoom}
|
||||
max={maxZoom}
|
||||
step="0.1"
|
||||
value={zoom}
|
||||
onChange={(e) => setZoom(Number(e.target.value))}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user