Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_gallery.js
T
DemonGatanjieuandClaude Opus 5.5 054cb91b9b style: one palette for every page, from the theme tokens
Pages made at different times used their own greys (slate here, neutral
there) and accents (sky, violet, indigo), and most ignored the Abyssal
Scarlet theme. About 1,850 literal colours in web/styles and in CSS and
inline styles written from JS now use the --amb-* tokens by what they are
for: surfaces, text (new --amb-text-soft and --amb-text-dim), borders (new
--amb-border-strong), the primary button, and one accent (new --amb-link,
tinted with color-mix). Both themes define the new tokens.

Literal colours stay where the colour is the meaning: status, prompt
roles, voice emotions, canvas node colours, shadows and overlays; theme
override rules are untouched. The share-code dialogs move from inline
styles to .anomalous-share-* classes. Stylesheet versions are bumped.

Checked: text contrast on every rail page, Prompt Studio, Prompt Notes,
Model Sources, settings, model detail and editor, share codes, in both
themes; structure check and JS tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:19:55 +08:00

725 lines
33 KiB
JavaScript

/**
* ui_gallery.js
* Extracted Gallery Viewer methods.
*/
import { app } from "../../../scripts/app.js";
import { translate } from './locales.js';
import { showImageMaterialDetail } from './ui_materials.js';
import { createSearchChips } from './ui_search_chips.js';
const t = (key, params) => translate(key, params);
export async function refreshGalleryImages() {
if (this.galleryLoading || this.galleryRefreshLoading) return;
this.galleryRefreshLoading = true;
try {
const scrollTop = this.galleryGrid?.scrollTop || 0;
await this.loadGalleryImages(1, true);
if (this.galleryGrid) this.galleryGrid.scrollTop = scrollTop;
} catch (error) {
console.warn('Could not refresh output gallery:', error);
} finally {
this.galleryRefreshLoading = false;
}
}
/** Search blocks above the output gallery; the terms live on the browser as `gallerySearchTerms`. */
export function createGallerySearchBar(owner) {
const search = createSearchChips({
placeholder: t('gallerySearchPlaceholder'),
help: t('gallerySearchHelp'),
onChange: (terms) => {
owner.gallerySearchTerms = terms;
owner.loadGalleryImages(1, true, { refresh: false });
},
});
search.element.classList.add('anomalous-gallery-search');
owner.gallerySearchCount = search.countElement;
return search.element;
}
export async function loadGalleryImages(page = 1, reset = false, { refresh = reset } = {}) {
if (this.galleryLoading) {
// A newer query arrived while loading: run it once the current request settles.
if (reset) this.galleryReloadPending = true;
return;
}
this.galleryLoading = true;
const terms = [...(this.gallerySearchTerms || [])];
const termsKey = JSON.stringify(terms);
const query = terms.length > 0;
this.gallerySentinel.textContent = query && page === 1 ? t('gallerySearching') : t('galleryLoading');
try {
const params = new URLSearchParams({ page: String(page), limit: '50' });
if (refresh) params.set('refresh', '1');
terms.forEach(term => params.append('term', term));
const res = await fetch(`/anomalous/gallery_images?${params}`);
const data = await res.json();
if (termsKey !== JSON.stringify(this.gallerySearchTerms || [])) {
this.galleryReloadPending = true;
return;
}
if (this.gallerySearchCount) {
this.gallerySearchCount.textContent = query && Number.isFinite(data.total) ? t('gallerySearchCount', { count: data.total }) : '';
}
if (reset) {
// Clear existing cards
const cards = this.galleryGrid.querySelectorAll('.anomalous-gallery-card');
cards.forEach(c => c.remove());
this.galleryLoaded = true;
this.galleryImagesList = [];
}
if (data.images && data.images.length > 0) {
const incomingItems = data.images.map(imgData => {
const q_sub = encodeURIComponent(imgData.subfolder);
const q_file = encodeURIComponent(imgData.filename);
return {
filename: imgData.filename,
subfolder: imgData.subfolder || '',
url: `/view?filename=${q_file}&subfolder=${q_sub}&type=output`,
sourceImage: {
type: 'output',
filename: imgData.filename,
subfolder: imgData.subfolder || '',
}
};
});
if (reset) {
this.galleryImagesList = incomingItems;
} else {
this.galleryImagesList = [...(this.galleryImagesList || []), ...incomingItems];
}
data.images.forEach(imgData => {
const card = document.createElement('div');
card.className = 'anomalous-gallery-card';
const q_sub = encodeURIComponent(imgData.subfolder);
const q_file = encodeURIComponent(imgData.filename);
const imgUrl = `/view?filename=${q_file}&subfolder=${q_sub}&type=output`;
const img = document.createElement('img');
img.src = `/anomalous/output_thumbnail?filename=${q_file}&subfolder=${q_sub}`; // drags and the viewer use the original
img.loading = 'lazy';
img.draggable = true;
img.title = t('materialViewOriginal');
// Drag and drop support for ComfyUI
img.addEventListener('dragstart', (e) => {
const fullUrl = new URL(imgUrl, window.location.href).href;
e.dataTransfer.setData('text/uri-list', fullUrl);
e.dataTransfer.setData('text/plain', fullUrl);
// Fix for Chromium failing to initiate drag for extremely large (Hires Fix) images
if (window.anomalousDragGhostImg) {
e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
}
});
const openDetail = () => {
const curIdx = (this.galleryImagesList || []).findIndex(it => it.filename === imgData.filename && it.subfolder === (imgData.subfolder || ''));
void showImageMaterialDetail(this, {
type: 'output',
filename: imgData.filename,
subfolder: imgData.subfolder || '',
}, imgUrl, {
items: this.galleryImagesList || [],
currentIndex: curIdx >= 0 ? curIdx : 0,
loadMore: async () => {
if (this.galleryHasMore && !this.galleryLoading) {
await this.loadGalleryImages(this.galleryCurrentPage + 1);
}
return this.galleryImagesList || [];
}
});
};
// Click to view
img.onclick = () => {
if (this.gallerySelectModel) {
const model = this.gallerySelectModel;
fetch('/anomalous/set_custom_cover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: this.currentType,
path_idx: this.currentPathIdx,
subfolder: this.currentSubfolder,
filename: model.filename,
source_image: imgData.subfolder ? imgData.subfolder + '/' + imgData.filename : imgData.filename
})
}).then(res => res.json()).then(async data => {
if (data.status === 'success') {
const tempModel = this.gallerySelectModel;
this.gallerySelectModel = null;
const banner = document.getElementById('anomalous-gallery-select-banner');
if (banner) banner.style.display = 'none';
this.galleryPanel.classList.remove('is-cover-selecting');
this.galleryPanel.style.display = 'none';
await this.loadModels();
const updatedModel = this.models.find(m => m.filename === model.filename);
if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
this.detailPanel.style.display = 'flex';
if (updatedModel) this.showDetail(updatedModel);
} else {
this.grid.style.display = 'grid';
// Grid was already refreshed by loadModels
}
} else {
alert(t('galleryErrorPrefix') + data.message);
}
});
return;
}
showGalleryViewer(imgUrl);
};
const delBtn = document.createElement('button');
delBtn.className = 'anomalous-gallery-delete';
delBtn.innerHTML = '<svg style="width:14px;height:14px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
delBtn.title = t('galleryDelete');
delBtn.onclick = (e) => {
e.stopPropagation();
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.background = 'rgba(0,0,0,0.85)';
overlay.style.display = 'flex';
overlay.style.flexDirection = 'column';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.gap = '15px';
overlay.style.zIndex = '10';
const msg = document.createElement('div');
const msgTitle = document.createElement('div');
msgTitle.textContent = t('galleryDeleteConfirm');
const msgHint = document.createElement('div');
msgHint.textContent = t('galleryDeleteConfirmHint');
msgHint.style.fontSize = '0.8em';
msgHint.style.color = '#aaa';
msg.append(msgTitle, msgHint);
msg.style.color = '#fff';
msg.style.fontWeight = 'bold';
msg.style.textAlign = 'center';
const btnRow = document.createElement('div');
btnRow.style.display = 'flex';
btnRow.style.gap = '12px';
const confirmBtn = document.createElement('button');
confirmBtn.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>${t('galleryDelete')}`;
confirmBtn.style.background = '#dc3545';
confirmBtn.style.color = '#fff';
confirmBtn.style.border = 'none';
confirmBtn.style.padding = '10px 16px';
confirmBtn.style.borderRadius = '6px';
confirmBtn.style.cursor = 'pointer';
confirmBtn.style.fontWeight = 'bold';
confirmBtn.style.transition = 'background 0.2s';
confirmBtn.onmouseover = () => confirmBtn.style.background = '#ff0000';
confirmBtn.onmouseout = () => confirmBtn.style.background = '#dc3545';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = t('galleryCancel');
cancelBtn.style.background = 'var(--amb-bg-card-hover)';
cancelBtn.style.color = '#fff';
cancelBtn.style.border = 'none';
cancelBtn.style.padding = '10px 16px';
cancelBtn.style.borderRadius = '6px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.style.transition = 'background 0.2s';
cancelBtn.onmouseover = () => cancelBtn.style.background = '#666';
cancelBtn.onmouseout = () => cancelBtn.style.background = 'var(--amb-bg-card-hover)';
cancelBtn.onclick = (ce) => {
ce.stopPropagation();
overlay.remove();
};
confirmBtn.onclick = async (ce) => {
ce.stopPropagation();
confirmBtn.textContent = t('galleryDeleting');
confirmBtn.disabled = true;
try {
const dr = await fetch('/anomalous/delete_gallery_image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: imgData.filename, subfolder: imgData.subfolder })
});
const dd = await dr.json();
if (dd.status === 'success') {
card.remove();
if (this.galleryImagesList) {
this.galleryImagesList = this.galleryImagesList.filter(it => !(it.filename === imgData.filename && it.subfolder === (imgData.subfolder || '')));
}
} else {
alert(t('galleryDeleteFailed') + dd.message);
overlay.remove();
}
} catch (err) {
alert(t('galleryErrorPrefix') + err);
overlay.remove();
}
};
btnRow.appendChild(cancelBtn);
btnRow.appendChild(confirmBtn);
overlay.appendChild(msg);
overlay.appendChild(btnRow);
card.appendChild(overlay);
};
const detailsBtn = document.createElement('button');
detailsBtn.className = 'anomalous-gallery-details';
detailsBtn.type = 'button';
detailsBtn.innerHTML = `
<svg class="anomalous-gallery-details-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
<line x1="2" y1="5" x2="8" y2="5"></line>
<line x1="12" y1="5" x2="14" y2="5"></line>
<circle cx="10" cy="5" r="2"></circle>
<line x1="2" y1="11" x2="4" y2="11"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
<circle cx="6" cy="11" r="2"></circle>
</svg>
<span>${t('materialViewParameters')}</span>
`;
detailsBtn.title = t('materialViewDetails');
detailsBtn.onclick = (event) => {
event.stopPropagation();
if (this.gallerySelectModel) return;
openDetail();
};
card.appendChild(img);
card.appendChild(detailsBtn);
card.appendChild(delBtn);
this.galleryGrid.insertBefore(card, this.gallerySentinel);
});
this.galleryCurrentPage = page;
this.galleryHasMore = page < data.pages;
if (!this.galleryHasMore) {
this.gallerySentinel.textContent = t('galleryNoMore');
} else {
this.gallerySentinel.textContent = t('galleryScrollMore');
}
} else {
this.galleryHasMore = false;
this.gallerySentinel.textContent = reset ? t(query ? 'gallerySearchEmpty' : 'galleryEmpty') : t('galleryNoMore');
}
} catch (e) {
console.error('Failed to load gallery images', e);
this.gallerySentinel.textContent = t('galleryLoadFailed');
} finally {
this.galleryLoading = false;
if (this.galleryReloadPending) {
this.galleryReloadPending = false;
void this.loadGalleryImages(1, true, { refresh: false });
}
}
}
export async function showGeneratedGallery(model) {
let overlay = document.getElementById('anomalous-generated-gallery-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'anomalous-generated-gallery-overlay';
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
overlay.style.zIndex = '999999';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
const modalBox = document.createElement('div');
modalBox.id = 'anomalous-generated-gallery-modal';
modalBox.style.width = '95%';
modalBox.style.maxHeight = '95%';
modalBox.style.backgroundColor = 'var(--comfy-menu-bg, var(--amb-bg-card))';
modalBox.style.borderRadius = '12px';
modalBox.style.display = 'flex';
modalBox.style.flexDirection = 'column';
modalBox.style.overflow = 'hidden';
modalBox.style.boxShadow = '0 10px 40px rgba(0, 0, 0, 0.8)';
const header = document.createElement('div');
header.style.padding = '15px 25px';
header.style.background = 'var(--amb-bg-card-hover)';
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.borderBottom = '1px solid var(--amb-border-strong)';
const title = document.createElement('h2');
title.id = 'anomalous-generated-gallery-title';
title.style.margin = '0';
title.style.color = '#fff';
const closeBtn = document.createElement('button');
closeBtn.textContent = `✖ ${t('galleryClose')}`;
closeBtn.style.padding = '8px 15px';
closeBtn.style.background = '#dc3545';
closeBtn.style.color = '#fff';
closeBtn.style.border = 'none';
closeBtn.style.borderRadius = '5px';
closeBtn.style.cursor = 'pointer';
closeBtn.style.fontWeight = 'bold';
closeBtn.onmouseover = () => closeBtn.style.background = '#c82333';
closeBtn.onmouseout = () => closeBtn.style.background = '#dc3545';
closeBtn.onclick = () => {
overlay.style.display = 'none';
};
header.appendChild(title);
header.appendChild(closeBtn);
modalBox.appendChild(header);
const contentCont = document.createElement('div');
contentCont.id = 'anomalous-generated-gallery-content';
contentCont.style.flex = '1';
contentCont.style.overflowY = 'auto';
contentCont.style.padding = '20px';
contentCont.style.display = 'grid';
contentCont.style.gridTemplateColumns = 'repeat(auto-fill, minmax(220px, 1fr))';
contentCont.style.gap = '25px';
contentCont.style.rowGap = '40px';
contentCont.style.alignContent = 'start';
modalBox.appendChild(contentCont);
overlay.appendChild(modalBox);
document.getElementById('anomalous-container').appendChild(overlay);
}
const title = document.getElementById('anomalous-generated-gallery-title');
title.textContent = t('galleryHistoryTitle', { name: model.name || model.filename });
const contentCont = document.getElementById('anomalous-generated-gallery-content');
contentCont.innerHTML = '';
const loading = document.createElement('div');
loading.textContent = t('galleryScanning');
loading.style.textAlign = 'center';
loading.style.gridColumn = '1 / -1';
loading.style.padding = '50px';
loading.style.color = '#aaa';
contentCont.appendChild(loading);
overlay.style.display = 'flex';
try {
const res = await fetch('/anomalous/model_images?model_name=' + encodeURIComponent(model.filename) + '&t=' + Date.now());
const data = await res.json();
contentCont.innerHTML = '';
if (!data.images || data.images.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.textContent = t('galleryNoModelImages');
emptyMsg.style.textAlign = 'center';
emptyMsg.style.gridColumn = '1 / -1';
emptyMsg.style.padding = '50px';
emptyMsg.style.color = '#888';
contentCont.appendChild(emptyMsg);
return;
}
data.images.forEach(img => {
const imgCont = document.createElement('div');
imgCont.className = 'anomalous-card';
imgCont.style.cursor = 'pointer';
const el = document.createElement('img');
el.src = img.url || img; // Support both just in case
el.loading = 'lazy';
el.draggable = true;
el.addEventListener('dragstart', (e) => {
const fullUrl = new URL(el.src, window.location.href).href;
e.dataTransfer.setData('text/uri-list', fullUrl);
e.dataTransfer.setData('text/plain', fullUrl);
if (window.anomalousDragGhostImg) {
e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
}
});
let source_image = "";
let filenameText = "";
if (img.url) {
try {
const urlParams = new URLSearchParams(img.url.split('?')[1]);
filenameText = urlParams.get('filename') || '';
const sub = urlParams.get('subfolder') || '';
source_image = sub ? sub + '/' + filenameText : filenameText;
} catch (e) { }
} else {
filenameText = img.split('/').pop().split('?')[0];
source_image = filenameText;
}
const titleDiv = document.createElement('div');
titleDiv.className = 'anomalous-card-title';
titleDiv.innerText = filenameText;
const setCoverBtn = document.createElement('button');
setCoverBtn.textContent = t('gallerySetCover');
setCoverBtn.style.position = 'absolute';
setCoverBtn.style.bottom = '40px';
setCoverBtn.style.right = '5px';
setCoverBtn.style.background = 'rgba(40, 167, 69, 0.85)';
setCoverBtn.style.color = '#fff';
setCoverBtn.style.border = '1px solid var(--amb-border-strong)';
setCoverBtn.style.borderRadius = '4px';
setCoverBtn.style.padding = '4px 8px';
setCoverBtn.style.cursor = 'pointer';
setCoverBtn.style.zIndex = '10';
setCoverBtn.style.fontSize = '12px';
setCoverBtn.onclick = (e) => {
e.stopPropagation();
if (!confirm(t('gallerySetCoverConfirm'))) return;
fetch('/anomalous/set_custom_cover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: this.currentType,
path_idx: this.currentPathIdx,
subfolder: this.currentSubfolder,
filename: model.filename,
source_image: source_image
})
}).then(res => res.json()).then(async result => {
if (result.status === 'success') {
alert(t('galleryCoverSuccess'));
await this.loadModels();
const updatedModel = this.models.find(m => m.filename === model.filename);
if (updatedModel && this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
this.showDetail(updatedModel);
}
} else {
alert(t('galleryErrorPrefix') + result.message);
}
}).catch(err => {
alert(t('galleryErrorPrefix') + err.message);
});
};
imgCont.onclick = () => {
const allGenCards = Array.from(contentCont.querySelectorAll('.anomalous-card'));
const curIndex = allGenCards.indexOf(imgCont);
const genItems = (data.images || []).map(im => {
const u = im.url || im;
let fn = '';
let sub = '';
if (im.url) {
try {
const up = new URLSearchParams(im.url.split('?')[1]);
fn = up.get('filename') || '';
sub = up.get('subfolder') || '';
} catch (_) {}
} else {
fn = String(im).split('/').pop().split('?')[0];
}
return {
filename: fn,
subfolder: sub,
url: u,
sourceImage: { type: 'output', filename: fn, subfolder: sub }
};
});
void showImageMaterialDetail(this, {
type: 'output',
filename: filenameText,
subfolder: source_image.includes('/') ? source_image.split('/')[0] : '',
}, img.url || img, {
items: genItems,
currentIndex: curIndex >= 0 ? curIndex : 0,
});
};
imgCont.appendChild(el);
imgCont.appendChild(setCoverBtn);
imgCont.appendChild(titleDiv);
contentCont.appendChild(imgCont);
});
} catch (e) {
const error = document.createElement('div');
error.textContent = t('galleryLoadImagesError');
error.style.color = 'red';
error.style.textAlign = 'center';
error.style.gridColumn = '1 / -1';
error.style.padding = '50px';
contentCont.replaceChildren(error);
}
}
export function showGallerySelectMode(model) {
this.gallerySelectModel = model;
this.grid.style.display = 'none';
this.detailPanel.style.display = 'none';
this.galleryPanel.style.display = 'flex';
this.galleryPanel.classList.add('is-cover-selecting');
let banner = document.getElementById('anomalous-gallery-select-banner');
if (!banner) {
banner = document.createElement('div');
banner.id = 'anomalous-gallery-select-banner';
banner.style.background = '#28a745';
banner.style.color = '#fff';
banner.style.padding = '10px';
banner.style.textAlign = 'center';
banner.style.fontWeight = 'bold';
banner.style.position = 'sticky';
banner.style.top = '0';
banner.style.zIndex = '1000';
this.galleryPanel.insertBefore(banner, this.galleryPanel.firstChild);
}
banner.style.display = 'block';
banner.replaceChildren();
const bannerPrefix = document.createTextNode(t('gallerySelectingCoverPrefix'));
const bannerModel = document.createElement('span');
bannerModel.textContent = model.filename;
bannerModel.style.color = '#ff0';
const bannerSuffix = document.createTextNode(t('gallerySelectingCoverSuffix'));
const cancelSelect = document.createElement('button');
cancelSelect.id = 'anomalous-cancel-select';
cancelSelect.textContent = t('galleryCancel');
cancelSelect.style.marginLeft = '15px';
cancelSelect.style.color = '#000';
cancelSelect.style.background = '#fff';
cancelSelect.style.border = 'none';
cancelSelect.style.padding = '2px 8px';
cancelSelect.style.borderRadius = '4px';
cancelSelect.style.cursor = 'pointer';
banner.append(bannerPrefix, bannerModel, bannerSuffix, cancelSelect);
cancelSelect.onclick = () => {
const tempModel = this.gallerySelectModel;
this.gallerySelectModel = null;
this.galleryPanel.classList.remove('is-cover-selecting');
banner.style.display = 'none';
this.galleryPanel.style.display = 'none';
if (this.currentDetailModel) {
this.detailPanel.style.display = 'flex';
} else {
this.grid.style.display = 'grid';
}
if (tempModel) {
this.showEditModal(tempModel);
}
};
if (!this.galleryLoaded) {
this.loadGalleryImages(1, true);
}
}
export function showGalleryViewer(src) {
let viewer = document.getElementById('anomalous-gallery-viewer');
if (!viewer) {
viewer = document.createElement('dialog');
viewer.id = 'anomalous-gallery-viewer';
viewer.className = 'anomalous-gallery-viewer';
const closeBtn = document.createElement('div');
closeBtn.className = 'anomalous-gallery-viewer-close';
closeBtn.innerHTML = '&times;';
const img = document.createElement('img');
img.id = 'anomalous-gallery-viewer-img';
viewer.appendChild(img);
viewer.appendChild(closeBtn);
let scale = 1;
let translateX = 0;
let translateY = 0;
let isDragging = false;
let startX = 0, startY = 0;
const resetImgTransform = () => {
scale = 1; translateX = 0; translateY = 0;
img.style.transform = `translate(0px, 0px) scale(1)`;
img.style.cursor = 'grab';
};
closeBtn.onclick = () => {
viewer.close();
resetImgTransform();
};
viewer.onclick = (e) => {
if (e.target === viewer) {
viewer.close();
resetImgTransform();
}
};
viewer.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomFactor = 0.1;
if (e.deltaY < 0) scale += zoomFactor;
else scale = Math.max(0.1, scale - zoomFactor);
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
});
img.addEventListener('mousedown', (e) => {
e.preventDefault();
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
img.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
translateX = e.clientX - startX;
translateY = e.clientY - startY;
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
});
window.addEventListener('mouseup', () => {
isDragging = false;
img.style.cursor = 'grab';
});
document.body.appendChild(viewer);
}
const img = document.getElementById('anomalous-gallery-viewer-img');
img.src = src;
// Reset scale and translation when opening a new image
img.style.transform = `translate(0px, 0px) scale(1)`;
img.style.cursor = 'grab';
if (!viewer.open) viewer.showModal();
}