import { useState, useEffect, useCallback, useRef } from 'react'; import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose, RotateCcw } from 'lucide-react'; import { Button, OGDialog, OGDialogContent, TooltipAnchor } from '~/components'; import { useLocalize } from '~/hooks'; const getQualityStyles = (quality: string): string => { if (quality === 'high') { return 'bg-green-100 text-green-800'; } if (quality === 'low') { return 'bg-orange-100 text-orange-800'; } return 'bg-gray-100 text-gray-800'; }; export default function DialogImage({ isOpen, onOpenChange, src = '', downloadImage, args }) { const localize = useLocalize(); const [isPromptOpen, setIsPromptOpen] = useState(false); const [imageSize, setImageSize] = useState(null); // Zoom and pan state const [zoom, setZoom] = useState(1); const [panX, setPanX] = useState(0); const [panY, setPanY] = useState(0); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const containerRef = useRef(null); const getImageSize = useCallback(async (url: string) => { try { const response = await fetch(url, { method: 'HEAD' }); const contentLength = response.headers.get('Content-Length'); if (contentLength) { const bytes = parseInt(contentLength, 10); return formatFileSize(bytes); } const fullResponse = await fetch(url); const blob = await fullResponse.blob(); return formatFileSize(blob.size); } catch (error) { console.error('Error getting image size:', error); return null; } }, []); const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getImageMaxWidth = () => { // On mobile (when panel overlays), use full width minus padding // On desktop, account for the side panel width if (isPromptOpen) { return window.innerWidth >= 640 ? 'calc(100vw - 22rem)' : 'calc(100vw - 2rem)'; } return 'calc(100vw - 2rem)'; }; const resetZoom = useCallback(() => { setZoom(1); setPanX(0); setPanY(0); }, []); const getCursor = () => { if (zoom <= 1) return 'default'; return isDragging ? 'grabbing' : 'grab'; }; const handleDoubleClick = useCallback(() => { if (zoom > 1) { resetZoom(); } else { // Zoom in to 2x on double click when at normal zoom setZoom(2); } }, [zoom, resetZoom]); const handleWheel = useCallback( (e: React.WheelEvent) => { e.preventDefault(); if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // Calculate zoom factor const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = Math.min(Math.max(zoom * zoomFactor, 1), 5); if (newZoom === zoom) return; // If zooming back to 1, reset pan to center the image if (newZoom === 1) { setZoom(1); setPanX(0); setPanY(0); return; } // Calculate the zoom center relative to the current viewport const containerCenterX = rect.width / 2; const containerCenterY = rect.height / 2; // Calculate new pan position to zoom towards mouse cursor const zoomRatio = newZoom / zoom; const deltaX = (mouseX - containerCenterX - panX) * (zoomRatio - 1); const deltaY = (mouseY - containerCenterY - panY) * (zoomRatio - 1); setZoom(newZoom); setPanX(panX - deltaX); setPanY(panY - deltaY); }, [zoom, panX, panY], ); const handleMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); if (zoom <= 1) return; setIsDragging(true); setDragStart({ x: e.clientX - panX, y: e.clientY - panY, }); }, [zoom, panX, panY], ); const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (!isDragging || zoom <= 1) return; const newPanX = e.clientX - dragStart.x; const newPanY = e.clientY - dragStart.y; setPanX(newPanX); setPanY(newPanY); }, [isDragging, dragStart, zoom], ); const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === 'Escape' && resetZoom(); document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [resetZoom]); useEffect(() => { if (isOpen && src) { getImageSize(src).then(setImageSize); resetZoom(); } }, [isOpen, src, getImageSize, resetZoom]); // Ensure image is centered when zoom changes to 1 useEffect(() => { if (zoom === 1) { setPanX(0); setPanY(0); } }, [zoom]); // Reset pan when panel opens/closes to maintain centering useEffect(() => { if (zoom === 1) { setPanX(0); setPanY(0); } }, [isPromptOpen, zoom]); return (
onOpenChange(false)} variant="ghost" className="h-10 w-10 p-0 hover:bg-surface-hover" > } />
{zoom > 1 && ( } /> )} downloadImage()} variant="ghost" className="h-10 w-10 p-0"> } /> setIsPromptOpen(!isPromptOpen)} variant="ghost" className="h-10 w-10 p-0" > {isPromptOpen ? ( ) : ( )} } />
{/* Main content area with image */}
1 ? 'hidden' : 'visible', minHeight: 0, // Allow flexbox to shrink }} >
Image
{/* Side Panel */}
{/* Mobile pull handle - removed for cleaner look */}
{/* Mobile close button */}

{localize('com_ui_image_details')}

{localize('com_ui_image_details')}

{/* Prompt Section */}

{localize('com_ui_prompt')}

{args?.prompt || 'No prompt available'}

{/* Generation Settings */}

{localize('com_ui_generation_settings')}

{localize('com_ui_size')}: {args?.size || 'Unknown'}
{localize('com_ui_quality')}: {args?.quality || 'Standard'}
{localize('com_ui_file_size')}: {imageSize || 'Loading...'}
); }