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);
|
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;
|
if (!pendingItemData) return;
|
||||||
|
|
||||||
const { index } = pendingItemData;
|
const { index } = pendingItemData;
|
||||||
|
|
||||||
if (adjustments && extractedItems[index]) {
|
if (adjustments && extractedItems[index]) {
|
||||||
// Override AI-detected values with user adjustments
|
|
||||||
const updatedItems = [...extractedItems];
|
const updatedItems = [...extractedItems];
|
||||||
updatedItems[index] = {
|
updatedItems[index] = {
|
||||||
...updatedItems[index],
|
...updatedItems[index],
|
||||||
@@ -82,11 +81,27 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
|||||||
user_adjusted: true
|
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);
|
setShowImageAdjustment(false);
|
||||||
setAdjustingItemIndex(null);
|
setAdjustingItemIndex(null);
|
||||||
setPendingItemData(null);
|
setPendingItemData(null);
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import { X, RotateCw } from 'lucide-react';
|
|||||||
|
|
||||||
interface ImageAdjustmentModalProps {
|
interface ImageAdjustmentModalProps {
|
||||||
imageUrl: string;
|
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;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdjustmentModalProps) {
|
export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdjustmentModalProps) {
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
const [zoom, setZoom] = useState(1);
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const [minZoom, setMinZoom] = useState(0.1);
|
||||||
|
const [maxZoom, setMaxZoom] = useState(5);
|
||||||
const [panX, setPanX] = useState(0);
|
const [panX, setPanX] = useState(0);
|
||||||
const [panY, setPanY] = useState(0);
|
const [panY, setPanY] = useState(0);
|
||||||
const [useImage, setUseImage] = useState(true);
|
const [useImage, setUseImage] = useState(true);
|
||||||
@@ -51,8 +53,9 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
|||||||
const canvasHeight = 600;
|
const canvasHeight = 600;
|
||||||
const zoomX = canvasWidth / img.width;
|
const zoomX = canvasWidth / img.width;
|
||||||
const zoomY = canvasHeight / img.height;
|
const zoomY = canvasHeight / img.height;
|
||||||
const fitZoom = Math.min(zoomX, zoomY, 1); // Don't zoom in, only out to fit
|
const fitZoom = Math.min(zoomX, zoomY); // Allow any zoom needed to fit
|
||||||
setZoom(fitZoom);
|
setMinZoom(fitZoom); // Min zoom shows entire image
|
||||||
|
setZoom(fitZoom); // Start at fit level
|
||||||
};
|
};
|
||||||
img.src = imageUrl;
|
img.src = imageUrl;
|
||||||
}, [imageUrl]);
|
}, [imageUrl]);
|
||||||
@@ -115,18 +118,45 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = async () => {
|
||||||
if (useImage && cropBounds) {
|
if (!useImage || !cropBounds || !imageRef.current) {
|
||||||
onConfirm({ rotation, cropBounds });
|
|
||||||
} else {
|
|
||||||
onConfirm(null);
|
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>) => {
|
const handleWheel = (e: React.WheelEvent<HTMLCanvasElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
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) => {
|
const getTouchDistance = (touches: React.TouchList) => {
|
||||||
@@ -154,7 +184,7 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
|||||||
const newDistance = getTouchDistance(e.touches);
|
const newDistance = getTouchDistance(e.touches);
|
||||||
if (lastTouchDistance > 0) {
|
if (lastTouchDistance > 0) {
|
||||||
const delta = newDistance / lastTouchDistance;
|
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);
|
setLastTouchDistance(newDistance);
|
||||||
}
|
}
|
||||||
} else if (e.touches.length === 1 && isPanning) {
|
} else if (e.touches.length === 1 && isPanning) {
|
||||||
@@ -213,8 +243,8 @@ export function ImageAdjustmentModal({ imageUrl, onConfirm, onCancel }: ImageAdj
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0.5"
|
min={minZoom}
|
||||||
max="5"
|
max={maxZoom}
|
||||||
step="0.1"
|
step="0.1"
|
||||||
value={zoom}
|
value={zoom}
|
||||||
onChange={(e) => setZoom(Number(e.target.value))}
|
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