import { Flex, Overlay, Text } from "@mantine/core"; import { ImageToView } from "./ImageViewerProvider"; import { useState } from "react"; import { IconChevronLeft, IconChevronRight, IconImageInPicture, IconX } from "@tabler/icons-react"; import { useRosettaColors } from "@/app/hooks/useRosettaColors"; import { useEffect } from "react"; import { useContextMenu } from "../ContextMenuProvider/useContextMenu"; import { convertJpegBlobToPngBlob, createBlobFromBase64Image } from "@/app/utils/utils"; interface ImageViewerProps { images: ImageToView[]; initialSlide: number; onClose: () => void; } export function ImageViewer(props : ImageViewerProps) { const [slide, setSlide] = useState(props.initialSlide); const imageToRender = props.images[slide]; const colors = useRosettaColors(); const openContextMenu = useContextMenu(); const [pos, setPos] = useState({ x: 0, y: 0, scale: 1 }); const [isDragging, setIsDragging] = useState(false); const [wasDragging, setWasDragging] = useState(false); const [dragStart, setDragStart] = useState<{x: number, y: number} | null>(null); const isNextSlideAvailable = slide + 1 <= props.images.length - 1; const isPrevSlideAvailable = slide - 1 >= 0; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowLeft" && isPrevSlideAvailable) { prevSlide(e); } if (e.key === "ArrowRight" && isNextSlideAvailable) { nextSlide(e); } if (e.key === "Escape") { props.onClose(); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [slide, isPrevSlideAvailable, isNextSlideAvailable, props.onClose]); useEffect(() => { setPos({ x: 0, y: 0, scale: 1 }); }, [slide]); const nextSlide = (e : any) => { e.stopPropagation(); if(slide + 1 > props.images.length - 1) { return; } setSlide(slide + 1); } const prevSlide = (e : any) => { e.stopPropagation(); if(slide - 1 < 0) { return; } setSlide(slide - 1); } const onContextMenuImg = async () => { let blob = await convertJpegBlobToPngBlob( createBlobFromBase64Image(imageToRender.src) ); openContextMenu([ { label: 'Copy Image', action: async () => { await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); }, icon: } ]); } // Wheel zoom (zoom to cursor) const onWheel = (e: React.WheelEvent) => { //e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const prevScale = pos.scale; let newScale = prevScale - e.deltaY * 0.004; // ускоренное увеличение newScale = Math.max(0.2, Math.min(5, newScale)); const offsetX = mouseX - pos.x; const offsetY = mouseY - pos.y; const newX = mouseX - (offsetX * newScale) / prevScale; const newY = mouseY - (offsetY * newScale) / prevScale; setPos({ ...pos, scale: newScale, x: newX, y: newY }); }; // Drag logic const onMouseDown = (e: React.MouseEvent) => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); setIsDragging(true); setWasDragging(false); setDragStart({ x: e.clientX - pos.x, y: e.clientY - pos.y }); }; const onMouseMove = (e: React.MouseEvent) => { if (!isDragging || !dragStart) return; setPos({ ...pos, x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); setWasDragging(true); }; const onMouseUp = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); setDragStart(null); }; return ( { if (!isDragging && !wasDragging) props.onClose(); }} onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} backgroundOpacity={0.7} > onContextMenuImg()} src={imageToRender.src} style={{ maxWidth: '70vw', maxHeight: '70vh', borderRadius: 8, userSelect: 'none', cursor: isDragging ? 'grabbing' : 'grab', transformOrigin: '0 0', transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`, }} onWheel={onWheel} onMouseDown={onMouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} onMouseLeave={onMouseUp} draggable={false} /> {slide + 1} of {props.images.length} {imageToRender.timestamp && {new Date(imageToRender.timestamp).toLocaleString()} } ); }