From 383898eae7655212b2016455b62c333b1e33d875 Mon Sep 17 00:00:00 2001 From: Ryan Meador <1176600+ryanmeador@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:31:21 -0500 Subject: [PATCH 1/3] fix gallery modal next/prev button navigation --- javascript/gallery.js | 65 ++++++++++++++++++++++++++++++++++++--- javascript/imageViewer.js | 18 ++++++++++- javascript/ui.js | 4 +++ 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index 4405da465..72180bf15 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -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) => { diff --git a/javascript/imageViewer.js b/javascript/imageViewer.js index 8788ca689..deba7df3a 100644 --- a/javascript/imageViewer.js +++ b/javascript/imageViewer.js @@ -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) { diff --git a/javascript/ui.js b/javascript/ui.js index 9c3fb4f8e..a7b58c8d0 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -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; } From a38e5af3b36d89a946df54e2c3f6b1776b883165 Mon Sep 17 00:00:00 2001 From: Ryan Meador <1176600+ryanmeador@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:43:03 -0500 Subject: [PATCH 2/3] add selection indicator to gallery view --- javascript/gallery.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/javascript/gallery.js b/javascript/gallery.js index 72180bf15..76e9a6046 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -34,6 +34,7 @@ function refreshGallerySelection({ emit = false } = {}) { const current = window.currentImage; const index = files.findIndex((file) => file.src === current); gallerySelection = { files, index }; + updateGallerySelectionClasses(); if (emit) { document.dispatchEvent(new CustomEvent('gallery-selection-changed', { detail: { index, files } })); } @@ -46,6 +47,7 @@ function applyGallerySelection(index, { send = true, emit = true } = {}) { const clamped = Math.max(0, Math.min(index, files.length - 1)); gallerySelection.index = clamped; window.currentImage = files[clamped].src; + updateGallerySelectionClasses(); if (send && el.btnSend) el.btnSend.click(); if (emit) { document.dispatchEvent(new CustomEvent('gallery-selection-changed', { detail: { index: clamped, files } })); @@ -65,12 +67,20 @@ function setGallerySelectionByElement(element, options) { function resetGallerySelection() { gallerySelection = { files: [], index: -1 }; window.currentImage = null; + updateGallerySelectionClasses(); } function buildGalleryFileUrl(path) { return new URL(`/file=${encodeURI(path)}`, window.location.origin).toString(); } +function updateGallerySelectionClasses() { + const { files, index } = gallerySelection; + files.forEach((file, i) => { + file.classList.toggle('gallery-file-selected', i === index); + }); +} + window.getGallerySelection = () => ({ index: gallerySelection.index, files: gallerySelection.files }); window.setGallerySelection = (index, options) => applyGallerySelection(index, options); window.getGallerySelectedUrl = () => (window.currentImage ? buildGalleryFileUrl(window.currentImage) : null); @@ -154,6 +164,9 @@ function updateGalleryStyles() { .gallery-file:hover { filter: grayscale(100%); } + :host(.gallery-file-selected) .gallery-file { + box-shadow: 0 0 0 2px var(--sd-button-selected-color); + } `); } From 300bc31de723553bb1f4ba9aec784468cd1bea83 Mon Sep 17 00:00:00 2001 From: Ryan Meador <1176600+ryanmeador@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:04:04 -0500 Subject: [PATCH 3/3] address PR feedback --- javascript/gallery.js | 52 ++++++++++++++++++--------------------- javascript/imageViewer.js | 10 ++++---- javascript/ui.js | 4 +-- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index 76e9a6046..4159d8ad1 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1,7 +1,7 @@ /* eslint-disable max-classes-per-file */ let ws; let url; -window.currentImage = window.currentImage || null; +let currentImage = null; let pruneImagesTimer; let outstanding = 0; let lastSort = 0; @@ -29,29 +29,27 @@ function getVisibleGalleryFiles() { return Array.from(el.files.children).filter((node) => node.name && node.offsetParent); } -function refreshGallerySelection({ emit = false } = {}) { +function refreshGallerySelection() { + updateGallerySelectionClasses(gallerySelection.files, -1); const files = getVisibleGalleryFiles(); - const current = window.currentImage; - const index = files.findIndex((file) => file.src === current); + const index = files.findIndex((file) => file.src === currentImage); gallerySelection = { files, index }; - updateGallerySelectionClasses(); - if (emit) { - document.dispatchEvent(new CustomEvent('gallery-selection-changed', { detail: { index, files } })); - } + updateGallerySelectionClasses(files, index); } -function applyGallerySelection(index, { send = true, emit = true } = {}) { +function applyGallerySelection(index, { send = 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; - updateGallerySelectionClasses(); - if (send && el.btnSend) el.btnSend.click(); - if (emit) { - document.dispatchEvent(new CustomEvent('gallery-selection-changed', { detail: { index: clamped, files } })); + if (!Number.isInteger(index) || index < 0 || index >= files.length) { + log('gallery selection index out of range', index, files.length); + resetGallerySelection(); + return; } + gallerySelection.index = index; + currentImage = files[index].src; + updateGallerySelectionClasses(files, index); + if (send && el.btnSend) el.btnSend.click(); } function setGallerySelectionByElement(element, options) { @@ -65,17 +63,16 @@ function setGallerySelectionByElement(element, options) { } function resetGallerySelection() { + updateGallerySelectionClasses(gallerySelection.files, -1); gallerySelection = { files: [], index: -1 }; - window.currentImage = null; - updateGallerySelectionClasses(); + currentImage = null; } function buildGalleryFileUrl(path) { return new URL(`/file=${encodeURI(path)}`, window.location.origin).toString(); } -function updateGallerySelectionClasses() { - const { files, index } = gallerySelection; +function updateGallerySelectionClasses(files = gallerySelection.files, index = gallerySelection.index) { files.forEach((file, i) => { file.classList.toggle('gallery-file-selected', i === index); }); @@ -83,7 +80,7 @@ function updateGallerySelectionClasses() { window.getGallerySelection = () => ({ index: gallerySelection.index, files: gallerySelection.files }); window.setGallerySelection = (index, options) => applyGallerySelection(index, options); -window.getGallerySelectedUrl = () => (window.currentImage ? buildGalleryFileUrl(window.currentImage) : null); +window.getGallerySelectedUrl = () => (currentImage ? buildGalleryFileUrl(currentImage) : null); /** * Wait for the `outstanding` variable to be below the specified value @@ -586,7 +583,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 = () => { - setGallerySelectionByElement(this, { send: true, emit: true }); + setGallerySelectionByElement(this, { send: true }); }; img.title = `Folder: ${this.folder}\nFile: ${this.name}\nSize: ${this.size.toLocaleString()} bytes\nModified: ${this.mtime.toLocaleString()}`; if (this.shadow.children.length > 0) { @@ -606,7 +603,7 @@ class GalleryFile extends HTMLElement { // methods -const gallerySendImage = (_images) => [window.currentImage]; // invoked by gradio button +const gallerySendImage = (_images) => [currentImage]; // invoked by gradio button async function getHash(str, algo = 'SHA-256') { try { @@ -789,7 +786,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 }); + refreshGallerySelection(); }, 250); } @@ -910,7 +907,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 }); + refreshGallerySelection(); } /** @@ -1018,7 +1015,6 @@ 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; @@ -1047,7 +1043,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 }); + refreshGallerySelection(); thumbCacheCleanup(evt.target.name, numFiles, controller); } @@ -1110,7 +1106,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 }); + refreshGallerySelection(); thumbCacheCleanup(evt.target.name, numFiles, controller); }; ws.onerror = (event) => { diff --git a/javascript/imageViewer.js b/javascript/imageViewer.js index deba7df3a..53caa3c6c 100644 --- a/javascript/imageViewer.js +++ b/javascript/imageViewer.js @@ -53,15 +53,15 @@ function modalImageSwitch(offset) { 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 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 }); + window.setGallerySelection(nextIndex, { send: 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) { + const directSrc = window.getGallerySelectedUrl(); + if (modalImage && modal && directSrc) { modalImage.src = directSrc; if (modalImage.style.display === 'none') modal.style.setProperty('background-image', `url(${directSrc})`); } diff --git a/javascript/ui.js b/javascript/ui.js index a7b58c8d0..9934cbf83 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -64,8 +64,8 @@ function selected_gallery_index() { 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; + const gallerySelection = window.getGallerySelection(); + if (Number.isInteger(gallerySelection.index)) result = gallerySelection.index; } return result; }