From a344a13863507dd803a0f7c05c820dfa05add987 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:31:37 -0800 Subject: [PATCH 01/10] Move AbortController reset to function --- javascript/gallery.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index fb81bb58a..aa1471c98 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -24,6 +24,13 @@ const el = { const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr']; +function resetController(reason) { + maintenanceController.abort(reason); + const controller = new AbortController(); + maintenanceController = controller; + return controller; +} + function getVisibleGalleryFiles() { if (!el.files) return []; return Array.from(el.files.children).filter((node) => node.name && node.offsetParent); @@ -1049,9 +1056,8 @@ async function fetchFilesHT(evt, controller) { async function fetchFilesWS(evt) { // fetch file-by-file list over websockets if (!url) return; - const controller = new AbortController(); // Only called here because fetchFilesHT isn't called directly - maintenanceController.abort('Gallery update'); // Abort previous controller - maintenanceController = controller; // Point to new controller for next time + // Abort previous controller and point to new controller for next time + const controller = resetController('Gallery update'); // Called here because fetchFilesHT isn't called directly galleryHashes.clear(); // Must happen AFTER the AbortController steps galleryProgressBar.clear(); resetGallerySelection(); From 2fae55a7f99161e9a93ef4ada6da23e829051f62 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:38:24 -0800 Subject: [PATCH 02/10] Initial thumbnail cache clearing setup --- javascript/gallery.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index aa1471c98..73aea7dd2 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -2,6 +2,7 @@ let ws; let url; let currentImage = null; +let currentGalleryFolder = null; let pruneImagesTimer; let outstanding = 0; let lastSort = 0; @@ -959,9 +960,10 @@ const maintenanceQueue = new SimpleFunctionQueue('Maintenance'); * @param {string} folder - Folder to clean * @param {number} imgCount - Expected number of images in gallery * @param {AbortController} controller - AbortController that's handling this task + * @param {boolean} force - Force full cleanup of the folder */ -async function thumbCacheCleanup(folder, imgCount, controller) { - if (!opts.browser_cache) return; +async function thumbCacheCleanup(folder, imgCount, controller, force = false) { + if (!opts.browser_cache && !force) return; try { if (typeof folder !== 'string' || typeof imgCount !== 'number') { throw new Error('Function called with invalid arguments'); @@ -978,14 +980,14 @@ async function thumbCacheCleanup(folder, imgCount, controller) { callback: async () => { log(`Thumbnail DB cleanup: Checking if "${folder}" needs cleaning`); const t0 = performance.now(); - const staticGalleryHashes = new Set(galleryHashes); // External context should be safe since this function run is guarded by AbortController/AbortSignal in the SimpleFunctionQueue + const staticGalleryHashes = new Set(galleryHashes.values()); // External context should be safe since this function run is guarded by AbortController/AbortSignal in the SimpleFunctionQueue const cachedHashesCount = await idbCount(folder) .catch((e) => { error(`Thumbnail DB cleanup: Error when getting entry count for "${folder}".`, e); return Infinity; // Forces next check to fail if something went wrong }); const cleanupCount = cachedHashesCount - staticGalleryHashes.size; - if (cleanupCount < 500 || !Number.isFinite(cleanupCount)) { + if (!force && (cleanupCount < 500 || !Number.isFinite(cleanupCount))) { // Don't run when there aren't many excess entries return; } @@ -1019,6 +1021,20 @@ async function thumbCacheCleanup(folder, imgCount, controller) { }); } +async function addCacheClearButton() { + const btn = document.createElement('button'); + btn.innerText = 'Clear Folder Thumbnails (double click)'; + btn.addEventListener('dblclick', () => { + if (!currentGalleryFolder) return; + const controller = resetController('Clearing thumbnails'); + galleryHashes.clear(); + galleryProgressBar.clear(); + resetGallerySelection(); + thumbCacheCleanup(currentGalleryFolder, 0, controller, true); + }); + el.files.insertAdjacentElement('afterend', btn); +} + async function fetchFilesHT(evt, controller) { const t0 = performance.now(); const fragment = document.createDocumentFragment(); @@ -1074,6 +1090,7 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets return; } log(`gallery: connected=${wsConnected} state=${ws?.readyState} url=${ws?.url}`); + currentGalleryFolder = evt.target.name; if (!wsConnected) { await fetchFilesHT(evt, controller); // fallback to http return; @@ -1184,6 +1201,7 @@ async function initGallery() { // triggered on gradio change to monitor when ui updateGalleryStyles(); injectGalleryStatusCSS(); setOverlayAnimation(); + addCacheClearButton(); const progress = gradioApp().getElementById('tab-gallery-progress'); if (progress) { galleryProgressBar.attachTo(progress); From cb0aa2fb97e2f27435b54bbeece42a798f874a27 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:45:04 -0800 Subject: [PATCH 03/10] Update layout --- javascript/gallery.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index 73aea7dd2..004c3208b 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1022,7 +1022,10 @@ async function thumbCacheCleanup(folder, imgCount, controller, force = false) { } async function addCacheClearButton() { + const div = document.createElement('div'); + div.style.marginBlock = '0.5rem'; const btn = document.createElement('button'); + btn.style.cssText = 'margin: auto; display: block;'; btn.innerText = 'Clear Folder Thumbnails (double click)'; btn.addEventListener('dblclick', () => { if (!currentGalleryFolder) return; @@ -1032,7 +1035,8 @@ async function addCacheClearButton() { resetGallerySelection(); thumbCacheCleanup(currentGalleryFolder, 0, controller, true); }); - el.files.insertAdjacentElement('afterend', btn); + div.append(btn); + el.files.insertAdjacentElement('afterend', div); } async function fetchFilesHT(evt, controller) { From 849f04530154125d1db3db4b747fb69dda6e9717 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:45:43 -0800 Subject: [PATCH 04/10] Clear gallery image list when running --- javascript/gallery.js | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/gallery.js b/javascript/gallery.js index 004c3208b..1621ef10f 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1033,6 +1033,7 @@ async function addCacheClearButton() { galleryHashes.clear(); galleryProgressBar.clear(); resetGallerySelection(); + el.files.innerHTML = ''; thumbCacheCleanup(currentGalleryFolder, 0, controller, true); }); div.append(btn); From 603560c079ea68cf9f78e04f3c63e0cea900537b Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:35:04 -0800 Subject: [PATCH 05/10] Move setup, update style --- javascript/gallery.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index 1621ef10f..ceef2a570 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -21,6 +21,7 @@ const el = { search: undefined, status: undefined, btnSend: undefined, + clearCache: undefined, }; const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr']; @@ -1023,7 +1024,7 @@ async function thumbCacheCleanup(folder, imgCount, controller, force = false) { async function addCacheClearButton() { const div = document.createElement('div'); - div.style.marginBlock = '0.5rem'; + div.style.cssText = 'margin-top: 1.5rem; margin-bottom: 0.5rem;'; const btn = document.createElement('button'); btn.style.cssText = 'margin: auto; display: block;'; btn.innerText = 'Clear Folder Thumbnails (double click)'; @@ -1038,6 +1039,7 @@ async function addCacheClearButton() { }); div.append(btn); el.files.insertAdjacentElement('afterend', div); + el.clearCache = div; } async function fetchFilesHT(evt, controller) { @@ -1096,6 +1098,9 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets } log(`gallery: connected=${wsConnected} state=${ws?.readyState} url=${ws?.url}`); currentGalleryFolder = evt.target.name; + if (!el.clearCache) { + addCacheClearButton(); + } if (!wsConnected) { await fetchFilesHT(evt, controller); // fallback to http return; @@ -1206,7 +1211,6 @@ async function initGallery() { // triggered on gradio change to monitor when ui updateGalleryStyles(); injectGalleryStatusCSS(); setOverlayAnimation(); - addCacheClearButton(); const progress = gradioApp().getElementById('tab-gallery-progress'); if (progress) { galleryProgressBar.attachTo(progress); From 5e8ea5217728b129e329716bc5d115045f12ecbe Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:10:26 -0800 Subject: [PATCH 06/10] Toggle visibility from settings --- javascript/gallery.js | 9 ++++++++- modules/shared.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index ceef2a570..266ce8f0d 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1022,7 +1022,7 @@ async function thumbCacheCleanup(folder, imgCount, controller, force = false) { }); } -async function addCacheClearButton() { +function addCacheClearButton() { // Don't use async const div = document.createElement('div'); div.style.cssText = 'margin-top: 1.5rem; margin-bottom: 0.5rem;'; const btn = document.createElement('button'); @@ -1042,6 +1042,12 @@ async function addCacheClearButton() { el.clearCache = div; } +async function updateGalleryAdvDisplay(newval, oldval = undefined) { + if (el.clearCache) { + el.clearCache.style.display = newval ? 'block' : 'none'; + } +} + async function fetchFilesHT(evt, controller) { const t0 = performance.now(); const fragment = document.createDocumentFragment(); @@ -1101,6 +1107,7 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets if (!el.clearCache) { addCacheClearButton(); } + updateGalleryAdvDisplay(opts.browser_gallery_advanced); if (!wsConnected) { await fetchFilesHT(evt, controller); // fallback to http return; diff --git a/modules/shared.py b/modules/shared.py index 0ac6511a1..c851f5b7c 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -533,6 +533,7 @@ options_templates.update(options_section(('saving-images', "Image Options"), { "image_sep_browser": OptionInfo("

Image Gallery

", "", gr.HTML), "browser_cache": OptionInfo(True, "Use image gallery cache"), + "browser_gallery_advanced": OptionInfo(False, "Show advanced features (below gallery)", gr.Checkbox), "browser_folders": OptionInfo("", "Additional image browser folders"), "browser_fixed_width": OptionInfo(False, "Use fixed width thumbnails"), "viewer_show_metadata": OptionInfo(True, "Show metadata in full screen image browser"), From 58347af9983489dd2df25b4e954d2f2050ff5fa2 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:12:17 -0800 Subject: [PATCH 07/10] Fix logic error and update syntax --- javascript/settings.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/javascript/settings.js b/javascript/settings.js index 537bfd7f0..ed564b29c 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -25,17 +25,15 @@ async function updateOpts(json_string) { const t1 = performance.now(); for (const op of monitoredOpts) { - const key = Object.keys(op)[0]; - const callback = op[key]; - if (opts[key] && opts[key] !== settings_data.values[key]) { - log('updateOpt', key, opts[key], settings_data.values[key]); + const [key, callback] = Object.entries(op)[0]; + if (Object.hasOwn(opts, key) && opts[key] !== new_opts[key]) { + log('updateOpt', key, opts[key], new_opts[key]); if (callback) callback(new_opts[key], opts[key]); } } for (const op of AppyOpts) { - const key = Object.keys(op)[0]; - const callback = op[key]; + const [key, callback] = Object.entries(op)[0]; if (callback) callback(new_opts[key], opts[key]); } From 7fc18befc5c99990dc3b101231fbc7fd9c5e56cd Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:12:36 -0800 Subject: [PATCH 08/10] Add `monitorOption` function --- eslint.config.mjs | 1 + javascript/gallery.js | 1 + javascript/settings.js | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index fddda6ca1..cd9ee3251 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -53,6 +53,7 @@ const jsConfig = defineConfig([ generateForever: 'readonly', showContributors: 'readonly', opts: 'writable', + monitorOption: 'readonly', sortUIElements: 'readonly', all_gallery_buttons: 'readonly', selected_gallery_button: 'readonly', diff --git a/javascript/gallery.js b/javascript/gallery.js index 266ce8f0d..85d849462 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1234,6 +1234,7 @@ async function initGallery() { // triggered on gradio change to monitor when ui }); intersectionObserver.observe(el.folders); monitorGalleries(); + monitorOption('browser_gallery_advanced', updateGalleryAdvDisplay); } // register on startup diff --git a/javascript/settings.js b/javascript/settings.js index ed564b29c..9aa22c626 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -11,6 +11,10 @@ const monitoredOpts = [ { sd_backend: () => gradioApp().getElementById('refresh_sd_model_checkpoint')?.click() }, ]; +function monitorOption(option, callback) { + monitoredOpts.push({ [option]: callback }); +} + const AppyOpts = [ { compact_view: (val, old) => toggleCompact(val, old) }, { gradio_theme: (val, old) => setTheme(val, old) }, From 5613cb383ae6d0fc8f224b8b052402160fb8fafd Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:37:13 -0800 Subject: [PATCH 09/10] Update text --- javascript/gallery.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index 85d849462..2707e7418 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1027,7 +1027,7 @@ function addCacheClearButton() { // Don't use async div.style.cssText = 'margin-top: 1.5rem; margin-bottom: 0.5rem;'; const btn = document.createElement('button'); btn.style.cssText = 'margin: auto; display: block;'; - btn.innerText = 'Clear Folder Thumbnails (double click)'; + btn.innerText = 'Clear Folder Thumbnails (double click/tap)'; btn.addEventListener('dblclick', () => { if (!currentGalleryFolder) return; const controller = resetController('Clearing thumbnails'); From e63422ba16ec4128ae24ba0a28c57316a7964b8e Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:53:29 -0800 Subject: [PATCH 10/10] Move cache clear to Image Options in settings --- eslint.config.mjs | 1 - javascript/gallery.js | 68 ++++++++++++++++++++++++++---------------- javascript/settings.js | 4 --- modules/shared.py | 1 - 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index cd9ee3251..fddda6ca1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -53,7 +53,6 @@ const jsConfig = defineConfig([ generateForever: 'readonly', showContributors: 'readonly', opts: 'writable', - monitorOption: 'readonly', sortUIElements: 'readonly', all_gallery_buttons: 'readonly', selected_gallery_button: 'readonly', diff --git a/javascript/gallery.js b/javascript/gallery.js index 2707e7418..54131386f 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -21,7 +21,7 @@ const el = { search: undefined, status: undefined, btnSend: undefined, - clearCache: undefined, + clearCacheFolder: undefined, }; const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr']; @@ -1022,30 +1022,39 @@ async function thumbCacheCleanup(folder, imgCount, controller, force = false) { }); } -function addCacheClearButton() { // Don't use async - const div = document.createElement('div'); - div.style.cssText = 'margin-top: 1.5rem; margin-bottom: 0.5rem;'; - const btn = document.createElement('button'); - btn.style.cssText = 'margin: auto; display: block;'; - btn.innerText = 'Clear Folder Thumbnails (double click/tap)'; - btn.addEventListener('dblclick', () => { - if (!currentGalleryFolder) return; - const controller = resetController('Clearing thumbnails'); - galleryHashes.clear(); - galleryProgressBar.clear(); - resetGallerySelection(); - el.files.innerHTML = ''; - thumbCacheCleanup(currentGalleryFolder, 0, controller, true); - }); - div.append(btn); - el.files.insertAdjacentElement('afterend', div); - el.clearCache = div; +function clearGalleryFolderCache(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (!currentGalleryFolder) return; + el.clearCacheFolder.style.color = 'var(--color-green)'; + setTimeout(() => { + el.clearCacheFolder.style.color = 'var(--color-blue)'; + }, 1000); + const controller = resetController('Clearing thumbnails'); + galleryHashes.clear(); + galleryProgressBar.clear(); + resetGallerySelection(); + el.files.innerHTML = ''; + thumbCacheCleanup(currentGalleryFolder, 0, controller, true); } -async function updateGalleryAdvDisplay(newval, oldval = undefined) { - if (el.clearCache) { - el.clearCache.style.display = newval ? 'block' : 'none'; +function addCacheClearLabel() { // Don't use async + const setting = document.querySelector('#setting_browser_cache'); + if (setting) { + const div = document.createElement('div'); + div.style.marginBlock = '0.75rem'; + + const span = document.createElement('span'); + span.style.cssText = 'font-weight:bold; text-decoration:underline; cursor:pointer; color:var(--color-blue); user-select: none;'; + span.innerText = '