fix gallery modal next/prev button navigation

This commit is contained in:
Ryan Meador
2026-01-08 17:31:21 -05:00
parent ae357a5e49
commit 383898eae7
3 changed files with 82 additions and 5 deletions
+61 -4
View File
@@ -1,11 +1,12 @@
/* eslint-disable max-classes-per-file */
let ws;
let url;
let currentImage;
window.currentImage = window.currentImage || null;
let pruneImagesTimer;
let outstanding = 0;
let lastSort = 0;
let lastSortName = 'None';
let gallerySelection = { files: [], index: -1 };
const galleryHashes = new Set();
let maintenanceController = new AbortController();
const folderStylesheet = new CSSStyleSheet();
@@ -23,6 +24,57 @@ const el = {
const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr'];
function getVisibleGalleryFiles() {
if (!el.files) return [];
return Array.from(el.files.children).filter((node) => node.name && node.offsetParent);
}
function refreshGallerySelection({ emit = false } = {}) {
const files = getVisibleGalleryFiles();
const current = window.currentImage;
const index = files.findIndex((file) => file.src === current);
gallerySelection = { files, index };
if (emit) {
document.dispatchEvent(new CustomEvent('gallery-selection-changed', { detail: { index, files } }));
}
}
function applyGallerySelection(index, { send = true, emit = true } = {}) {
if (!gallerySelection.files.length) refreshGallerySelection();
const files = gallerySelection.files;
if (!files.length) return;
const clamped = Math.max(0, Math.min(index, files.length - 1));
gallerySelection.index = clamped;
window.currentImage = files[clamped].src;
if (send && el.btnSend) el.btnSend.click();
if (emit) {
document.dispatchEvent(new CustomEvent('gallery-selection-changed', { detail: { index: clamped, files } }));
}
}
function setGallerySelectionByElement(element, options) {
if (!gallerySelection.files.length) refreshGallerySelection();
let index = gallerySelection.files.findIndex((file) => file === element);
if (index < 0) {
refreshGallerySelection();
index = gallerySelection.files.findIndex((file) => file === element);
}
if (index >= 0) applyGallerySelection(index, options);
}
function resetGallerySelection() {
gallerySelection = { files: [], index: -1 };
window.currentImage = null;
}
function buildGalleryFileUrl(path) {
return new URL(`/file=${encodeURI(path)}`, window.location.origin).toString();
}
window.getGallerySelection = () => ({ index: gallerySelection.index, files: gallerySelection.files });
window.setGallerySelection = (index, options) => applyGallerySelection(index, options);
window.getGallerySelectedUrl = () => (window.currentImage ? buildGalleryFileUrl(window.currentImage) : null);
/**
* Wait for the `outstanding` variable to be below the specified value
* @param {number} num - Threshold for `outstanding`
@@ -521,8 +573,7 @@ class GalleryFile extends HTMLElement {
return;
} // ... to here unless modifications are also being made to maintenance functionality and the usage of AbortController/AbortSignal
img.onclick = () => {
currentImage = this.src;
el.btnSend.click();
setGallerySelectionByElement(this, { send: true, emit: true });
};
img.title = `Folder: ${this.folder}\nFile: ${this.name}\nSize: ${this.size.toLocaleString()} bytes\nModified: ${this.mtime.toLocaleString()}`;
if (this.shadow.children.length > 0) {
@@ -542,7 +593,7 @@ class GalleryFile extends HTMLElement {
// methods
const gallerySendImage = (_images) => [currentImage]; // invoked by gradio button
const gallerySendImage = (_images) => [window.currentImage]; // invoked by gradio button
async function getHash(str, algo = 'SHA-256') {
try {
@@ -725,6 +776,7 @@ async function gallerySearch() {
const t1 = performance.now();
updateStatusWithSort('Filter', ['Images', `${totalFound.toLocaleString()} / ${allFiles.length.toLocaleString()}`], `${iconStopwatch} ${Math.floor(t1 - t0).toLocaleString()}ms`);
refreshGallerySelection({ emit: true });
}, 250);
}
@@ -845,6 +897,7 @@ async function gallerySort(btn) {
const t1 = performance.now();
log(`gallerySort: char=${lastSort} len=${arr.length} time=${Math.floor(t1 - t0)} sort=${lastSortName}`);
updateStatusWithSort(['Images', arr.length.toLocaleString()], `${iconStopwatch} ${Math.floor(t1 - t0).toLocaleString()}ms`);
refreshGallerySelection({ emit: true });
}
/**
@@ -952,6 +1005,7 @@ async function thumbCacheCleanup(folder, imgCount, controller) {
async function fetchFilesHT(evt, controller) {
const t0 = performance.now();
const fragment = document.createDocumentFragment();
resetGallerySelection();
updateStatusWithSort(['Folder', evt.target.name], 'in-progress');
let numFiles = 0;
@@ -980,6 +1034,7 @@ async function fetchFilesHT(evt, controller) {
updateStatusWithSort(['Folder', evt.target.name], ['Images', numFiles.toLocaleString()], `${iconStopwatch} ${Math.floor(t1 - t0).toLocaleString()}ms`);
galleryProgressBar.start(numFiles);
addSeparators();
refreshGallerySelection({ emit: true });
thumbCacheCleanup(evt.target.name, numFiles, controller);
}
@@ -990,6 +1045,7 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets
maintenanceController = controller; // Point to new controller for next time
galleryHashes.clear(); // Must happen AFTER the AbortController steps
galleryProgressBar.clear();
resetGallerySelection();
el.files.innerHTML = '';
updateGalleryStyles();
@@ -1041,6 +1097,7 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets
updateStatusWithSort(['Folder', evt.target.name], ['Images', numFiles.toLocaleString()], `${iconStopwatch} ${Math.floor(t1 - t0).toLocaleString()}ms`);
galleryProgressBar.start(numFiles);
addSeparators();
refreshGallerySelection({ emit: true });
thumbCacheCleanup(evt.target.name, numFiles, controller);
};
ws.onerror = (event) => {
+17 -1
View File
@@ -32,6 +32,7 @@ function closeModal(evt, force = false) {
}
function modalImageSwitch(offset) {
const negmod = (n, m) => ((n % m) + m) % m;
const galleryButtons = all_gallery_buttons();
if (galleryButtons.length > 1) {
const currentButton = selected_gallery_button();
@@ -39,7 +40,6 @@ function modalImageSwitch(offset) {
galleryButtons.forEach((v, i) => {
if (v === currentButton) result = i;
});
const negmod = (n, m) => ((n % m) + m) % m;
if (result !== -1) {
const nextButton = galleryButtons[negmod((result + offset), galleryButtons.length)];
nextButton.click();
@@ -47,8 +47,24 @@ function modalImageSwitch(offset) {
const modal = gradioApp().getElementById('lightboxModal');
modalImage.src = nextButton.children[0].src;
if (modalImage.style.display === 'none') modal.style.setProperty('background-image', `url(${modalImage.src})`);
return;
}
}
const galleryFilesContainer = gradioApp().getElementById('tab-gallery-files');
if (!galleryFilesContainer || !galleryFilesContainer.offsetParent) return;
const gallerySelection = window.getGallerySelection?.();
if (!gallerySelection?.files?.length || gallerySelection.files.length <= 1) return;
const baseIndex = gallerySelection.index >= 0 ? gallerySelection.index : 0;
const nextIndex = negmod((baseIndex + offset), gallerySelection.files.length);
window.setGallerySelection?.(nextIndex, { send: true, emit: true });
const modalImage = gradioApp().getElementById('modalImage');
const modal = gradioApp().getElementById('lightboxModal');
const directSrc = window.getGallerySelectedUrl?.() || new URL(`/file=${encodeURI(window.currentImage)}`, window.location.origin).toString();
if (modalImage && modal) {
modalImage.src = directSrc;
if (modalImage.style.display === 'none') modal.style.setProperty('background-image', `url(${directSrc})`);
}
}
function modalSaveImage(event) {
+4
View File
@@ -63,6 +63,10 @@ function selected_gallery_index() {
const button = selected_gallery_button();
let result = -1;
buttons.forEach((v, i) => { if (v === button) { result = i; } });
if (result === -1 && gradioApp().getElementById('tab-gallery-search')?.checkVisibility()) {
const gallerySelection = window.getGallerySelection?.();
if (gallerySelection && Number.isInteger(gallerySelection.index)) result = gallerySelection.index;
}
return result;
}