Files
arcum42-ComfyUI_SageUtils/js/shared/imageViewer.js
T

299 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { selectors } from './stateManager.js';
import { loadFullImage, openImageInNewTab as openImageInNewTabUtil } from './imageLoader.js';
import { createDialog } from '../components/dialogManager.js';
/**
* Show a full image modal viewer with navigation and zoom controls.
* @param {Object|string} imageInput - Image object or image path string
* @param {Array} [images] - Optional array of images for navigation
*/
export async function showFullImage(imageInput, imagesOrOptions = null) {
let imagePath;
let image;
let images = null;
let showMetadata = null;
if (Array.isArray(imagesOrOptions)) {
images = imagesOrOptions;
} else if (imagesOrOptions && typeof imagesOrOptions === 'object') {
if (Array.isArray(imagesOrOptions.images)) {
images = imagesOrOptions.images;
}
if (typeof imagesOrOptions.showMetadata === 'function') {
showMetadata = imagesOrOptions.showMetadata;
}
}
if (typeof imageInput === 'string') {
imagePath = imageInput;
if (!images) {
images = selectors.galleryImages() || [];
}
image = images.find(img =>
img.path === imagePath ||
img.relative_path === imagePath ||
img.name === imagePath.split('/').pop()
);
} else {
image = imageInput;
imagePath = image.path || image.relative_path || image.name;
}
if (!images) {
images = selectors.galleryImages() || [];
}
const currentIndex = images.findIndex(img =>
img.path === imagePath ||
img.relative_path === imagePath ||
img.name === (imagePath.includes('/') ? imagePath.split('/').pop() : imagePath)
);
if (currentIndex === -1) {
console.warn('Image not found in current set:', imagePath);
return;
}
let activeIndex = currentIndex;
let zoomLevel = 1;
let isDragging = false;
let startX = 0;
let startY = 0;
let translateX = 0;
let translateY = 0;
const dialog = createDialog({
title: image.filename || image.name || 'Image Viewer',
width: '90vw',
height: '90vh',
showCloseButton: true,
showFooter: false,
closeOnOverlayClick: true,
closable: true
});
const container = document.createElement('div');
container.style.cssText = `
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 12px;
overflow: hidden;
`;
const img = document.createElement('img');
img.style.cssText = `
max-width: 100%;
max-height: calc(100vh - 180px);
object-fit: contain;
transition: transform 0.1s ease;
cursor: grab;
`;
const controls = document.createElement('div');
controls.style.cssText = `
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
z-index: 10;
padding: 8px;
background: rgba(0, 0, 0, 0.5);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
`;
const buttonFactory = (text, title) => {
const button = document.createElement('button');
button.textContent = text;
button.title = title;
button.style.cssText = `
min-width: 44px;
min-height: 36px;
border-radius: 6px;
border: none;
background: rgba(0,0,0,0.6);
color: white;
cursor: pointer;
`;
return button;
};
const zoomInBtn = buttonFactory('+', 'Zoom In (+)');
const zoomOutBtn = buttonFactory('−', 'Zoom Out (-)');
const resetZoomBtn = buttonFactory('1:1', 'Reset Zoom (0)');
const metadataBtn = buttonFactory('ℹ️', 'Show Image Metadata');
const openNewTabBtn = buttonFactory('↗', 'Open in New Tab');
const prevBtn = buttonFactory('◀', 'Previous Image');
const nextBtn = buttonFactory('▶', 'Next Image');
const infoOverlay = document.createElement('div');
infoOverlay.style.cssText = `
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
color: white;
font-size: 12px;
letter-spacing: 0.2px;
opacity: 0.9;
text-align: center;
padding: 6px 10px;
background: rgba(0,0,0,0.35);
border-radius: 10px;
pointer-events: none;
`;
const updateTransform = () => {
img.style.transform = `scale(${zoomLevel}) translate(${translateX}px, ${translateY}px)`;
img.style.cursor = zoomLevel > 1 ? (isDragging ? 'grabbing' : 'grab') : 'default';
};
const getDisplayName = (image, imagePath) => {
return image.filename || image.name || imagePath?.split?.('/').pop() || 'Unknown Image';
};
const zoom = (factor) => {
const newZoom = Math.max(0.1, Math.min(5, zoomLevel * factor));
if (newZoom !== zoomLevel) {
zoomLevel = newZoom;
if (zoomLevel <= 1) {
translateX = 0;
translateY = 0;
}
updateTransform();
}
};
const resetZoom = () => {
zoomLevel = 1;
translateX = 0;
translateY = 0;
updateTransform();
};
const loadImage = async () => {
const currentImage = images[activeIndex];
if (!currentImage) return;
const imageName = getDisplayName(currentImage, currentImage.path || currentImage.relative_path || currentImage.preview || currentImage.name);
try {
const imageUrl = await loadFullImage(currentImage);
img.src = imageUrl;
} catch (error) {
console.error('Error loading full image:', error);
img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><rect width="400" height="300" fill="%23333"/><text x="200" y="150" text-anchor="middle" font-size="16" fill="%23999">Error loading image</text></svg>';
}
const sizeInfo = currentImage.width && currentImage.height ? ` (${currentImage.width}×${currentImage.height})` : '';
const indexInfo = images.length > 1 ? ` [${activeIndex + 1}/${images.length}]` : '';
dialog.setTitle(`${imageName}${sizeInfo}${indexInfo}`);
infoOverlay.textContent = `${imageName}${sizeInfo}${indexInfo}`;
};
const navigate = async (delta) => {
if (images.length <= 1) return;
activeIndex = Math.min(images.length - 1, Math.max(0, activeIndex + delta));
resetZoom();
await loadImage();
};
const handleMouseDown = (event) => {
if (zoomLevel <= 1) return;
isDragging = true;
startX = event.clientX - translateX;
startY = event.clientY - translateY;
img.style.cursor = 'grabbing';
event.preventDefault();
};
const handleMouseMove = (event) => {
if (!isDragging || zoomLevel <= 1) return;
translateX = event.clientX - startX;
translateY = event.clientY - startY;
updateTransform();
};
const handleMouseUp = () => {
if (!isDragging) return;
isDragging = false;
img.style.cursor = zoomLevel > 1 ? 'grab' : 'default';
};
const handleKeyDown = (event) => {
switch (event.key) {
case 'Escape':
dialog.close();
break;
case 'ArrowLeft':
navigate(-1);
break;
case 'ArrowRight':
navigate(1);
break;
case '+':
case '=':
zoom(1.2);
break;
case '-':
zoom(0.8);
break;
case '0':
resetZoom();
break;
}
};
zoomInBtn.addEventListener('click', () => zoom(1.2));
zoomOutBtn.addEventListener('click', () => zoom(0.8));
resetZoomBtn.addEventListener('click', resetZoom);
metadataBtn.addEventListener('click', () => {
if (typeof showMetadata === 'function') {
showMetadata(image);
} else {
import('../gallery/galleryEvents.js')
.then(({ showImageMetadata }) => {
if (typeof showImageMetadata === 'function') {
showImageMetadata(image);
} else {
console.warn('Image viewer metadata fallback did not expose showImageMetadata');
}
})
.catch((error) => {
console.warn('Failed to load gallery metadata helper:', error);
});
}
});
openNewTabBtn.addEventListener('click', () => openImageInNewTabUtil(image));
prevBtn.addEventListener('click', () => navigate(-1));
nextBtn.addEventListener('click', () => navigate(1));
controls.append(prevBtn, zoomInBtn, zoomOutBtn, resetZoomBtn, metadataBtn, openNewTabBtn, nextBtn);
container.append(img, controls, infoOverlay);
dialog.setContent(container);
dialog.show();
img.addEventListener('mousedown', handleMouseDown);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
window.addEventListener('keydown', handleKeyDown);
const originalClose = dialog.close.bind(dialog);
dialog.close = () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('keydown', handleKeyDown);
img.removeEventListener('mousedown', handleMouseDown);
originalClose();
};
await loadImage();
}