From 28b188f1efd060c17e1952bb440370596f32f4a7 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Wed, 29 Apr 2026 23:08:09 -0500 Subject: [PATCH 01/10] Adds multi-select support --- src/wwwroot/css/genpage.css | 13 + src/wwwroot/js/genpage/gentab/models.js | 3 +- .../js/genpage/gentab/outputhistory.js | 4 +- src/wwwroot/js/genpage/gentab/presets.js | 9 +- src/wwwroot/js/genpage/helpers/browsers.js | 275 ++++++++++++++++++ 5 files changed, 298 insertions(+), 6 deletions(-) diff --git a/src/wwwroot/css/genpage.css b/src/wwwroot/css/genpage.css index d3a23ee9b..6bcae5dd7 100644 --- a/src/wwwroot/css/genpage.css +++ b/src/wwwroot/css/genpage.css @@ -835,6 +835,19 @@ body { margin-left: 0.5rem; opacity: 0.8; } +.browser-multiselect-toggle-active { + outline: 2px solid var(--emphasis); + outline-offset: 1px; +} +.browser-multiselect-action-select { + max-width: 11rem; + margin-left: 0.15rem; + vertical-align: middle; +} +.browser-entry-multiselect-selected { + outline: 2px solid var(--emphasis); + outline-offset: 1px; +} .browser-fullcontent-container { margin-left: 0.2rem; display: inline-block; diff --git a/src/wwwroot/js/genpage/gentab/models.js b/src/wwwroot/js/genpage/gentab/models.js index 4aa7428f6..98bbbb71a 100644 --- a/src/wwwroot/js/genpage/gentab/models.js +++ b/src/wwwroot/js/genpage/gentab/models.js @@ -465,6 +465,7 @@ class ModelBrowserWrapper { let format = subType == 'Wildcards' ? 'Small Cards' : 'Cards'; extraHeader += ` `; this.browser = new GenPageBrowserClass(container, this.listModelFolderAndFiles.bind(this), id, format, this.describeModel.bind(this), this.selectModel.bind(this), extraHeader); + this.browser.enableBrowserMultiSelect = true; this.promptBox = getRequiredElementById('alt_prompt_textbox'); this.models = {}; this.browser.refreshHandler = (callback) => { @@ -717,7 +718,7 @@ class ModelBrowserWrapper { }, can_multi: true }]; } let isStarred = this.isStarred(model.data.name); - let starButton = { label: isStarred ? 'Unstar' : 'Star', onclick: () => { this.toggleStar(model.data.name); } }; + let starButton = { label: isStarred ? 'Unstar' : 'Star', onclick: () => { this.toggleStar(model.data.name); }, can_multi: true }; buttons.push(starButton); let name = cleanModelName(model.data.name); let display = (model.data.display || name).replaceAll('/', ' / '); diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index 25088870c..2edde4597 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -72,7 +72,8 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { className: (metadata && metaParsed.is_starred) ? ' star-button button-starred-image' : ' star-button', onclick: (e) => { toggleStar(fullsrc, src); - } + }, + can_multi: true }); buttons.push({ label: 'Enable Starred', @@ -247,6 +248,7 @@ function selectOutputInHistory(image, div) { let imageHistoryBrowser = new GenPageBrowserClass('image_history', listOutputHistoryFolderAndFiles, 'imagehistorybrowser', 'Thumbnails', describeOutputFile, selectOutputInHistory, ` `); +imageHistoryBrowser.enableBrowserMultiSelect = true; function storeImageToHistoryWithCurrentParams(img) { let data = getGenInput(); diff --git a/src/wwwroot/js/genpage/gentab/presets.js b/src/wwwroot/js/genpage/gentab/presets.js index 9daf9892b..30f338ff2 100644 --- a/src/wwwroot/js/genpage/gentab/presets.js +++ b/src/wwwroot/js/genpage/gentab/presets.js @@ -626,18 +626,18 @@ function listPresetFolderAndFiles(path, isRefresh, callback, depth) { function describePreset(preset) { let buttons = [ { label: 'Toggle', onclick: () => selectPreset(preset) }, - { label: 'Direct Apply', onclick: () => applyOnePreset(preset.data) }, - { label: preset.data.is_starred ? 'Unstar' : 'Star', onclick: () => togglePresetStar(preset) }, + { label: 'Direct Apply', onclick: () => applyOnePreset(preset.data), can_multi: true }, + { label: preset.data.is_starred ? 'Unstar' : 'Star', onclick: () => togglePresetStar(preset), can_multi: true }, { label: 'Edit Preset', onclick: () => editPreset(preset.data) }, { label: 'Duplicate Preset', onclick: () => duplicatePreset(preset.data) }, - { label: 'Export Preset', onclick: () => exportOnePresetButton(preset.data) }, + { label: 'Export Preset', onclick: () => exportOnePresetButton(preset.data), can_multi: true }, { label: 'Delete Preset', onclick: () => { if (confirm("Are you sure want to delete that preset?")) { genericRequest('DeletePreset', { preset: preset.data.title }, data => { loadUserData(); }); } - } } + }, can_multi: true } ]; let paramText = Object.keys(preset.data.param_map).map(key => `${key}: ${preset.data.param_map[key]}`); let description = `${preset.data.title}:\n${preset.data.description}\n\n${paramText.join('\n')}`; @@ -696,6 +696,7 @@ let presetBrowser = new GenPageBrowserClass('preset_list', listPresetFolderAndFi `); +presetBrowser.enableBrowserMultiSelect = true; function importPresetsButton() { getRequiredElementById('import_presets_textarea').value = ''; diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index 3f38fd88a..654bceb46 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -107,6 +107,11 @@ class GenPageBrowserClass { this.runAfterUpdate = []; this.refreshHandler = (callback) => callback(); this.checkIsSmall(); + this.enableBrowserMultiSelect = false; + this.multiSelectActive = false; + this.multiSelectedKeys = new Set(); + this.multiSelectToggleButton = null; + this.multiSelectActionSelect = null; } /** @@ -138,6 +143,10 @@ class GenPageBrowserClass { this.chunksRendered = 0; this.folder = folder; this.selected = null; + if (this.enableBrowserMultiSelect) { + this.multiSelectedKeys.clear(); + this.syncBrowserMultiSelectHeader(); + } this.update(false, callback); } @@ -455,6 +464,9 @@ class GenPageBrowserClass { } let img = document.createElement('img'); img.addEventListener('click', () => { + if (this.handleBrowserMultiSelectTileClick(file, div)) { + return; + } this.select(file, div); }); img.classList.add('image-block-img-inner'); @@ -466,6 +478,12 @@ class GenPageBrowserClass { let textBlock = createDiv(null, 'model-descblock'); textBlock.tabIndex = 0; textBlock.innerHTML = desc.description; + textBlock.addEventListener('click', (e) => { + if (this.handleBrowserMultiSelectTileClick(file, div, e)) { + return; + } + this.select(file, div); + }); div.appendChild(textBlock); } else if (this.format.includes('Thumbnails')) { @@ -495,6 +513,12 @@ class GenPageBrowserClass { else { textBlock.classList.add('image-preview-text-large'); } + textBlock.addEventListener('click', (e) => { + if (this.handleBrowserMultiSelectTileClick(file, div, e)) { + return; + } + this.select(file, div); + }); div.appendChild(textBlock); } else if (this.format == 'List') { @@ -502,6 +526,9 @@ class GenPageBrowserClass { let textBlock = createSpan(null, 'browser-list-entry-text'); textBlock.innerText = desc.display || desc.name; textBlock.addEventListener('click', () => { + if (this.handleBrowserMultiSelectTileClick(file, div)) { + return; + } this.select(file, div); }); div.appendChild(textBlock); @@ -520,6 +547,9 @@ class GenPageBrowserClass { textBlock.style.width = `calc(${percent}% - ${imgAdj}rem)`; textBlock.innerHTML = detail; textBlock.addEventListener('click', () => { + if (this.handleBrowserMultiSelectTileClick(file, div)) { + return; + } this.select(file, div); }); div.appendChild(textBlock); @@ -704,6 +734,37 @@ class GenPageBrowserClass { this.headerBar.appendChild(formatSelector); this.headerBar.appendChild(buttons); refreshButton.onclick = this.refresh.bind(this); + if (this.enableBrowserMultiSelect) { + this.multiSelectToggleButton = document.createElement('button'); + this.multiSelectToggleButton.type = 'button'; + this.multiSelectToggleButton.id = `${this.id}_multiselect_toggle`; + this.multiSelectToggleButton.className = 'refresh-button translate translate-no-text browser-multiselect-toggle'; + this.multiSelectToggleButton.title = 'Toggle multi-select mode'; + this.multiSelectToggleButton.innerHTML = '✓'; + this.multiSelectToggleButton.addEventListener('click', () => { + this.setBrowserMultiSelectActive(!this.multiSelectActive); + }); + this.multiSelectActionSelect = document.createElement('select'); + this.multiSelectActionSelect.id = `${this.id}_multiselect_action`; + this.multiSelectActionSelect.className = 'browser-format-selector browser-multiselect-action-select'; + this.multiSelectActionSelect.title = 'Bulk action'; + let placeholderOpt = document.createElement('option'); + placeholderOpt.value = ''; + placeholderOpt.className = 'translate'; + placeholderOpt.innerText = translate('Actions...'); + this.multiSelectActionSelect.appendChild(placeholderOpt); + this.multiSelectActionSelect.style.display = 'none'; + this.multiSelectActionSelect.addEventListener('change', () => { + let choice = this.multiSelectActionSelect.value; + if (!choice) { + return; + } + this.runBrowserMultiSelectAction(choice); + this.multiSelectActionSelect.value = ''; + }); + this.upButton.insertAdjacentElement('afterend', this.multiSelectToggleButton); + this.multiSelectToggleButton.insertAdjacentElement('afterend', this.multiSelectActionSelect); + } this.fullContentDiv.appendChild(this.headerBar); this.contentDiv = createDiv(`${this.id}-content`, 'browser-content-container'); this.contentDiv.addEventListener('scroll', () => { @@ -768,7 +829,10 @@ class GenPageBrowserClass { this.buildTreeElements(this.folderTreeDiv, '', this.tree); applyTranslations(this.headerBar); if (!this.noContentUpdates) { + this.pruneBrowserMultiSelectionToCurrentList(); this.buildContentList(this.contentDiv, files); + this.applyBrowserMultiSelectVisuals(); + this.syncBrowserMultiSelectHeader(); browserUtil.makeVisible(this.contentDiv); if (scrollOffset) { this.contentDiv.scrollTop = scrollOffset; @@ -783,4 +847,215 @@ class GenPageBrowserClass { this.builtEvent(); } } + + /** + * Removes all browser multi-select keys. + */ + clearBrowserMultiSelection() { + this.multiSelectedKeys.clear(); + this.syncBrowserMultiSelectHeader(); + if (this.contentDiv) { + this.applyBrowserMultiSelectVisuals(); + } + } + + /** + * Turns browser multi-select mode on or off; exiting clears the selection. + */ + setBrowserMultiSelectActive(active) { + if (!this.enableBrowserMultiSelect || this.multiSelectActive == active) { + return; + } + this.multiSelectActive = active; + if (!active) { + this.multiSelectedKeys.clear(); + } + if (this.multiSelectToggleButton) { + this.multiSelectToggleButton.classList.toggle('browser-multiselect-toggle-active', active); + } + this.syncBrowserMultiSelectHeader(); + if (this.contentDiv) { + this.applyBrowserMultiSelectVisuals(); + } + } + + /** + * Toggles whether a file row is selected for bulk actions. + */ + toggleBrowserMultiSelectForFile(file, div) { + let key = file.name; + if (this.multiSelectedKeys.has(key)) { + this.multiSelectedKeys.delete(key); + } + else { + this.multiSelectedKeys.add(key); + } + this.applyBrowserMultiSelectVisuals(); + this.syncBrowserMultiSelectHeader(); + } + + handleBrowserMultiSelectTileClick(file, div, event = null) { + if (!this.multiSelectActive || !this.enableBrowserMultiSelect) { + return false; + } + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.toggleBrowserMultiSelectForFile(file, div); + return true; + } + + /** + * Returns files in the current listing that are multi-selected. + */ + getMultiSelectedFiles() { + if (!this.lastFiles) { + return []; + } + let out = []; + for (let file of this.lastFiles) { + if (this.multiSelectedKeys.has(file.name)) { + out.push(file); + } + } + return out; + } + + /** + * Drops multi-select keys that no longer exist in the current lastFiles listing. + */ + pruneBrowserMultiSelectionToCurrentList() { + if (!this.lastFiles) { + return; + } + let names = new Set(this.lastFiles.map(f => f.name)); + for (let key of [...this.multiSelectedKeys]) { + if (!names.has(key)) { + this.multiSelectedKeys.delete(key); + } + } + } + + /** + * Labels for bulk actions shared by every selected item, respecting `can_multi` / `multi_only`. + */ + collectCommonBulkActionLabels() { + let files = this.getMultiSelectedFiles(); + let selCount = files.length; + if (selCount == 0) { + return []; + } + let eligiblePerFile = []; + for (let file of files) { + let desc = this.describe(file); + let labels = new Set(); + for (let button of desc.buttons) { + if (!button.onclick) { + continue; + } + if (button.multi_only && selCount < 2) { + continue; + } + if (!button.can_multi && !button.multi_only) { + continue; + } + labels.add(button.label); + } + eligiblePerFile.push(labels); + } + let first = eligiblePerFile[0]; + let common = []; + for (let label of first) { + if (eligiblePerFile.every(s => s.has(label))) { + common.push(label); + } + } + common.sort((a, b) => a.localeCompare(b)); + return common; + } + + /** + * Refreshes the bulk action dropdown from the current selection. + */ + syncBrowserMultiSelectHeader() { + if (!this.multiSelectActionSelect) { + return; + } + let show = this.multiSelectActive && this.multiSelectedKeys.size > 0; + this.multiSelectActionSelect.style.display = show ? '' : 'none'; + if (!show) { + return; + } + while (this.multiSelectActionSelect.options.length > 1) { + this.multiSelectActionSelect.remove(1); + } + let labels = this.collectCommonBulkActionLabels(); + for (let label of labels) { + let opt = document.createElement('option'); + opt.value = label; + opt.className = 'translate'; + opt.innerText = translate(label); + this.multiSelectActionSelect.appendChild(opt); + } + applyTranslations(this.multiSelectActionSelect); + } + + /** + * Runs a named bulk action (card popover label) once per selected item. + */ + runBrowserMultiSelectAction(label) { + let files = this.getMultiSelectedFiles(); + let failed = 0; + for (let file of files) { + let div = this.getVisibleEntry(file.name); + let desc = this.describe(file); + let button = null; + for (let b of desc.buttons) { + if (b.label == label && b.onclick) { + button = b; + break; + } + } + if (!button) { + failed++; + console.error(`No bulk action '${label}' for ${file.name}`); + continue; + } + try { + if (div) { + button.onclick(div); + } + else { + button.onclick(null); + } + } + catch (err) { + console.error('Browser bulk action error:', err); + failed++; + } + } + if (failed > 0) { + showError(`Bulk action finished: ${failed} of ${files.length} failed — see console for details.`); + } + this.pruneBrowserMultiSelectionToCurrentList(); + this.applyBrowserMultiSelectVisuals(); + this.syncBrowserMultiSelectHeader(); + } + + /** + * Applies multi-select highlight classes to visible rows. + */ + applyBrowserMultiSelectVisuals() { + if (!this.contentDiv) { + return; + } + for (let child of this.contentDiv.children) { + if (!child.dataset || !child.dataset.name) { + continue; + } + let on = this.multiSelectActive && this.multiSelectedKeys.has(child.dataset.name); + child.classList.toggle('browser-entry-multiselect-selected', on); + } + } } From 6e5b0107a9796d5e8b64248b84f972132f2cedda Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Wed, 29 Apr 2026 23:29:50 -0500 Subject: [PATCH 02/10] Update styling and activated icon --- src/wwwroot/css/genpage.css | 9 +++------ src/wwwroot/js/genpage/helpers/browsers.js | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/wwwroot/css/genpage.css b/src/wwwroot/css/genpage.css index 6bcae5dd7..9327d95d4 100644 --- a/src/wwwroot/css/genpage.css +++ b/src/wwwroot/css/genpage.css @@ -835,8 +835,9 @@ body { margin-left: 0.5rem; opacity: 0.8; } -.browser-multiselect-toggle-active { - outline: 2px solid var(--emphasis); +.browser-multiselect-toggle-active, +.browser-multiselect-entry-selected { + outline: 2px solid var(--emphasis) !important; outline-offset: 1px; } .browser-multiselect-action-select { @@ -844,10 +845,6 @@ body { margin-left: 0.15rem; vertical-align: middle; } -.browser-entry-multiselect-selected { - outline: 2px solid var(--emphasis); - outline-offset: 1px; -} .browser-fullcontent-container { margin-left: 0.2rem; display: inline-block; diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index 654bceb46..83ebfd983 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -870,9 +870,6 @@ class GenPageBrowserClass { if (!active) { this.multiSelectedKeys.clear(); } - if (this.multiSelectToggleButton) { - this.multiSelectToggleButton.classList.toggle('browser-multiselect-toggle-active', active); - } this.syncBrowserMultiSelectHeader(); if (this.contentDiv) { this.applyBrowserMultiSelectVisuals(); @@ -976,9 +973,22 @@ class GenPageBrowserClass { } /** - * Refreshes the bulk action dropdown from the current selection. + * Off: ✓ ✓ + * On: ☑ ☑ + */ + syncBrowserMultiSelectToggleAppearance() { + if (!this.multiSelectToggleButton) { + return; + } + this.multiSelectToggleButton.classList.toggle('browser-multiselect-toggle-active', this.multiSelectActive); + this.multiSelectToggleButton.innerHTML = this.multiSelectActive ? '☑' : '✓'; + } + + /** + * Updates multi-select toggle state and bulk action dropdown. */ syncBrowserMultiSelectHeader() { + this.syncBrowserMultiSelectToggleAppearance(); if (!this.multiSelectActionSelect) { return; } @@ -1055,7 +1065,7 @@ class GenPageBrowserClass { continue; } let on = this.multiSelectActive && this.multiSelectedKeys.has(child.dataset.name); - child.classList.toggle('browser-entry-multiselect-selected', on); + child.classList.toggle('browser-multiselect-entry-selected', on); } } } From db8e59200c9dcce9eedef52a81a2a0f585120823 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Wed, 29 Apr 2026 23:48:12 -0500 Subject: [PATCH 03/10] Adds max_selected --- src/wwwroot/js/genpage/gentab/outputhistory.js | 5 +++-- src/wwwroot/js/genpage/helpers/browsers.js | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index 2edde4597..6a1098e56 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -2,8 +2,8 @@ let registeredMediaButtons = []; /** Registers a media button for extensions. 'mediaTypes' filters by type eg ['audio'], null means all. 'isDefault' promotes to visible (vs More dropdown). 'showInHistory' controls whether button appears in the History panel. */ -function registerMediaButton(name, action, title = '', mediaTypes = null, isDefault = false, showInHistory = true, href = null, is_download = false, can_multi = false, multi_only = false) { - registeredMediaButtons.push({ name, action, title, mediaTypes, isDefault, showInHistory, href, is_download, can_multi, multi_only }); +function registerMediaButton(name, action, title = '', mediaTypes = null, isDefault = false, showInHistory = true, href = null, is_download = false, can_multi = false, multi_only = false, max_selected = null) { + registeredMediaButtons.push({ name, action, title, mediaTypes, isDefault, showInHistory, href, is_download, can_multi, multi_only, max_selected }); } function listOutputHistoryFolderAndFiles(path, isRefresh, callback, depth) { @@ -183,6 +183,7 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { is_download: reg.is_download, can_multi: reg.can_multi, multi_only: reg.multi_only, + max_selected: reg.max_selected, onclick: () => reg.action(src) }); } diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index 83ebfd983..bf399a41a 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -957,6 +957,9 @@ class GenPageBrowserClass { if (!button.can_multi && !button.multi_only) { continue; } + if (button.max_selected != null && selCount > button.max_selected) { + continue; + } labels.add(button.label); } eligiblePerFile.push(labels); From a7d7404be70cc66c31cc091f4912dcb5bf70bacf Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Thu, 30 Apr 2026 17:39:58 -0500 Subject: [PATCH 04/10] When multi-select button active, clicking card description selects it --- src/wwwroot/js/genpage/helpers/browsers.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index bf399a41a..e5c6dfb9e 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -478,13 +478,21 @@ class GenPageBrowserClass { let textBlock = createDiv(null, 'model-descblock'); textBlock.tabIndex = 0; textBlock.innerHTML = desc.description; - textBlock.addEventListener('click', (e) => { - if (this.handleBrowserMultiSelectTileClick(file, div, e)) { + div.appendChild(textBlock); + div.addEventListener('click', (e) => { + if (!this.enableBrowserMultiSelect || !this.multiSelectActive) { return; } - this.select(file, div); + if (e.target.closest('.model-block-menu-button')) { + return; + } + if (e.target.closest('img.image-block-img-inner')) { + return; + } + e.preventDefault(); + e.stopPropagation(); + this.toggleBrowserMultiSelectForFile(file, div); }); - div.appendChild(textBlock); } else if (this.format.includes('Thumbnails')) { div.className += ' image-block image-block-legacy'; From 284fe476af385efae4468ffc14b754519e241c2e Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Thu, 30 Apr 2026 18:21:07 -0500 Subject: [PATCH 05/10] multi-select works on batch container --- .../js/genpage/gentab/currentimagehandler.js | 5 ++ .../js/genpage/gentab/outputhistory.js | 43 ++++++++++++ src/wwwroot/js/genpage/helpers/browsers.js | 65 ++++++++++++------- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/wwwroot/js/genpage/gentab/currentimagehandler.js b/src/wwwroot/js/genpage/gentab/currentimagehandler.js index 460e7091c..19c9b3cf9 100644 --- a/src/wwwroot/js/genpage/gentab/currentimagehandler.js +++ b/src/wwwroot/js/genpage/gentab/currentimagehandler.js @@ -436,6 +436,11 @@ function toggleSeparateBatches() { function clickImageInBatch(div) { let imgElem = div.getElementsByTagName('img')[0]; + let multiSelectKey = getImageFullSrc(div.dataset.src); + if (imageHistoryBrowser.enableBrowserMultiSelect && imageHistoryBrowser.multiSelectActive && multiSelectKey && !div.classList.contains('image-block-placeholder')) { + imageHistoryBrowser.toggleBrowserMultiSelectForKey(multiSelectKey); + return; + } if (currentImgSrc == div.dataset.src) { imageFullView.showImage(div.dataset.src, div.dataset.metadata, div.dataset.batch_id); return; diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index 6a1098e56..a71d21a39 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -250,6 +250,49 @@ function selectOutputInHistory(image, div) { let imageHistoryBrowser = new GenPageBrowserClass('image_history', listOutputHistoryFolderAndFiles, 'imagehistorybrowser', 'Thumbnails', describeOutputFile, selectOutputInHistory, ` `); imageHistoryBrowser.enableBrowserMultiSelect = true; +imageHistoryBrowser.applyExtraMultiSelectVisuals = function() { + let currentImageBatchDiv = getRequiredElementById('current_image_batch'); + for (let block of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { + let blockKey = getImageFullSrc(block.dataset.src); + if (!blockKey) { + continue; + } + let on = this.multiSelectActive && this.multiSelectedKeys.has(blockKey); + block.classList.toggle('browser-multiselect-entry-selected', on); + } +}; + +imageHistoryBrowser.getExtraMultiSelectedFiles = function(seen) { + let out = []; + let currentImageBatchDiv = getRequiredElementById('current_image_batch'); + for (let key of this.multiSelectedKeys) { + if (seen.has(key)) { + continue; + } + seen.add(key); + let block = null; + for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { + if (getImageFullSrc(candidate.dataset.src) == key) { + block = candidate; + break; + } + } + let slash = key.lastIndexOf('/'); + let baseName = slash >= 0 ? key.substring(slash + 1) : key; + let src = block && block.dataset.src ? block.dataset.src : `${getImageOutPrefix()}/${key}`; + let metadata = block && block.dataset.metadata ? block.dataset.metadata : '{}'; + out.push({ + name: key, + data: { + src, + fullsrc: key, + name: baseName, + metadata + } + }); + } + return out; +}; function storeImageToHistoryWithCurrentParams(img) { let data = getGenInput(); diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index e5c6dfb9e..6b63e2575 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -862,9 +862,7 @@ class GenPageBrowserClass { clearBrowserMultiSelection() { this.multiSelectedKeys.clear(); this.syncBrowserMultiSelectHeader(); - if (this.contentDiv) { - this.applyBrowserMultiSelectVisuals(); - } + this.applyBrowserMultiSelectVisuals(); } /** @@ -879,16 +877,16 @@ class GenPageBrowserClass { this.multiSelectedKeys.clear(); } this.syncBrowserMultiSelectHeader(); - if (this.contentDiv) { - this.applyBrowserMultiSelectVisuals(); - } + this.applyBrowserMultiSelectVisuals(); } /** * Toggles whether a file row is selected for bulk actions. */ - toggleBrowserMultiSelectForFile(file, div) { - let key = file.name; + toggleBrowserMultiSelectForKey(key) { + if (!key) { + return; + } if (this.multiSelectedKeys.has(key)) { this.multiSelectedKeys.delete(key); } @@ -899,6 +897,13 @@ class GenPageBrowserClass { this.syncBrowserMultiSelectHeader(); } + /** + * Toggles whether a file row is selected for bulk actions. + */ + toggleBrowserMultiSelectForFile(file, div) { + this.toggleBrowserMultiSelectForKey(file.name); + } + handleBrowserMultiSelectTileClick(file, div, event = null) { if (!this.multiSelectActive || !this.enableBrowserMultiSelect) { return false; @@ -915,16 +920,17 @@ class GenPageBrowserClass { * Returns files in the current listing that are multi-selected. */ getMultiSelectedFiles() { - if (!this.lastFiles) { - return []; - } + let seen = new Set(); let out = []; - for (let file of this.lastFiles) { - if (this.multiSelectedKeys.has(file.name)) { - out.push(file); + if (this.lastFiles) { + for (let file of this.lastFiles) { + if (this.multiSelectedKeys.has(file.name)) { + out.push(file); + seen.add(file.name); + } } } - return out; + return out.concat(this.getExtraMultiSelectedFiles(seen)); } /** @@ -1064,19 +1070,32 @@ class GenPageBrowserClass { this.syncBrowserMultiSelectHeader(); } + /** + * Applies multi-select visuals outside the browser content list. + */ + applyExtraMultiSelectVisuals() { + } + + /** + * Returns extra multi-selected files outside the current browser listing. + */ + getExtraMultiSelectedFiles(seen) { + return []; + } + /** * Applies multi-select highlight classes to visible rows. */ applyBrowserMultiSelectVisuals() { - if (!this.contentDiv) { - return; - } - for (let child of this.contentDiv.children) { - if (!child.dataset || !child.dataset.name) { - continue; + if (this.contentDiv) { + for (let child of this.contentDiv.children) { + if (!child.dataset || !child.dataset.name) { + continue; + } + let on = this.multiSelectActive && this.multiSelectedKeys.has(child.dataset.name); + child.classList.toggle('browser-multiselect-entry-selected', on); } - let on = this.multiSelectActive && this.multiSelectedKeys.has(child.dataset.name); - child.classList.toggle('browser-multiselect-entry-selected', on); } + this.applyExtraMultiSelectVisuals(); } } From 5b32607253b4ba7ce0ac43ba5e5d8dce060568c7 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Thu, 30 Apr 2026 18:39:40 -0500 Subject: [PATCH 06/10] Delete multiple image fix --- .../js/genpage/gentab/currentimagehandler.js | 3 +++ .../js/genpage/gentab/outputhistory.js | 23 ++++++++++++++++--- src/wwwroot/js/genpage/helpers/browsers.js | 11 +++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/wwwroot/js/genpage/gentab/currentimagehandler.js b/src/wwwroot/js/genpage/gentab/currentimagehandler.js index 19c9b3cf9..dcbee7b50 100644 --- a/src/wwwroot/js/genpage/gentab/currentimagehandler.js +++ b/src/wwwroot/js/genpage/gentab/currentimagehandler.js @@ -791,6 +791,9 @@ function toggleStar(path, rawSrc) { imageFullView.showImage(rawSrc, JSON.stringify(newMetadata), imageFullView.currentBatchId); imageFullView.pasteState(state); } + if (imageHistoryBrowser.enableBrowserMultiSelect && imageHistoryBrowser.multiSelectActive) { + imageHistoryBrowser.syncBrowserMultiSelectHeader(); + } }); } diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index a71d21a39..18c2840b4 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -168,6 +168,11 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { if (div) { removeImageBlockFromBatch(div); } + if (imageHistoryBrowser.enableBrowserMultiSelect) { + imageHistoryBrowser.multiSelectedKeys.delete(fullsrc); + imageHistoryBrowser.applyBrowserMultiSelectVisuals(); + imageHistoryBrowser.syncBrowserMultiSelectHeader(); + } }); }, // TODO: Only ask once for the multi-set rather than once per each @@ -250,15 +255,27 @@ function selectOutputInHistory(image, div) { let imageHistoryBrowser = new GenPageBrowserClass('image_history', listOutputHistoryFolderAndFiles, 'imagehistorybrowser', 'Thumbnails', describeOutputFile, selectOutputInHistory, ` `); imageHistoryBrowser.enableBrowserMultiSelect = true; +imageHistoryBrowser.keepBrowserMultiSelectKeyAfterPrune = function(key, namesInCurrentList) { + if (namesInCurrentList.has(key)) { + return true; + } + let currentImageBatchDiv = getRequiredElementById('current_image_batch'); + for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { + if (getImageFullSrc(candidate.dataset.src) == key) { + return true; + } + } + return false; +}; imageHistoryBrowser.applyExtraMultiSelectVisuals = function() { let currentImageBatchDiv = getRequiredElementById('current_image_batch'); - for (let block of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { - let blockKey = getImageFullSrc(block.dataset.src); + for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { + let blockKey = getImageFullSrc(candidate.dataset.src); if (!blockKey) { continue; } let on = this.multiSelectActive && this.multiSelectedKeys.has(blockKey); - block.classList.toggle('browser-multiselect-entry-selected', on); + candidate.classList.toggle('browser-multiselect-entry-selected', on); } }; diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index 6b63e2575..96676556a 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -934,7 +934,14 @@ class GenPageBrowserClass { } /** - * Drops multi-select keys that no longer exist in the current lastFiles listing. + * Returns true if this multi-selected key should remain after pruning. + */ + keepBrowserMultiSelectKeyAfterPrune(key, namesInCurrentList) { + return namesInCurrentList.has(key); + } + + /** + * Removes multi-selected keys for which keepBrowserMultiSelectKeyAfterPrune returns false. */ pruneBrowserMultiSelectionToCurrentList() { if (!this.lastFiles) { @@ -942,7 +949,7 @@ class GenPageBrowserClass { } let names = new Set(this.lastFiles.map(f => f.name)); for (let key of [...this.multiSelectedKeys]) { - if (!names.has(key)) { + if (!this.keepBrowserMultiSelectKeyAfterPrune(key, names)) { this.multiSelectedKeys.delete(key); } } From c7625995a6d1764fa4157ac38a97329074ab55ed Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Sat, 2 May 2026 10:37:57 -0500 Subject: [PATCH 07/10] Reduce scope; pr feedback --- src/wwwroot/css/genpage.css | 2 +- .../js/genpage/gentab/currentimagehandler.js | 8 - src/wwwroot/js/genpage/gentab/models.js | 3 +- .../js/genpage/gentab/outputhistory.js | 87 +------- src/wwwroot/js/genpage/gentab/presets.js | 9 +- src/wwwroot/js/genpage/helpers/browsers.js | 207 +++++++----------- 6 files changed, 89 insertions(+), 227 deletions(-) diff --git a/src/wwwroot/css/genpage.css b/src/wwwroot/css/genpage.css index 9327d95d4..21cf8aefa 100644 --- a/src/wwwroot/css/genpage.css +++ b/src/wwwroot/css/genpage.css @@ -836,7 +836,7 @@ body { opacity: 0.8; } .browser-multiselect-toggle-active, -.browser-multiselect-entry-selected { +.browser-multiselect-item-selected { outline: 2px solid var(--emphasis) !important; outline-offset: 1px; } diff --git a/src/wwwroot/js/genpage/gentab/currentimagehandler.js b/src/wwwroot/js/genpage/gentab/currentimagehandler.js index dcbee7b50..460e7091c 100644 --- a/src/wwwroot/js/genpage/gentab/currentimagehandler.js +++ b/src/wwwroot/js/genpage/gentab/currentimagehandler.js @@ -436,11 +436,6 @@ function toggleSeparateBatches() { function clickImageInBatch(div) { let imgElem = div.getElementsByTagName('img')[0]; - let multiSelectKey = getImageFullSrc(div.dataset.src); - if (imageHistoryBrowser.enableBrowserMultiSelect && imageHistoryBrowser.multiSelectActive && multiSelectKey && !div.classList.contains('image-block-placeholder')) { - imageHistoryBrowser.toggleBrowserMultiSelectForKey(multiSelectKey); - return; - } if (currentImgSrc == div.dataset.src) { imageFullView.showImage(div.dataset.src, div.dataset.metadata, div.dataset.batch_id); return; @@ -791,9 +786,6 @@ function toggleStar(path, rawSrc) { imageFullView.showImage(rawSrc, JSON.stringify(newMetadata), imageFullView.currentBatchId); imageFullView.pasteState(state); } - if (imageHistoryBrowser.enableBrowserMultiSelect && imageHistoryBrowser.multiSelectActive) { - imageHistoryBrowser.syncBrowserMultiSelectHeader(); - } }); } diff --git a/src/wwwroot/js/genpage/gentab/models.js b/src/wwwroot/js/genpage/gentab/models.js index 98bbbb71a..4aa7428f6 100644 --- a/src/wwwroot/js/genpage/gentab/models.js +++ b/src/wwwroot/js/genpage/gentab/models.js @@ -465,7 +465,6 @@ class ModelBrowserWrapper { let format = subType == 'Wildcards' ? 'Small Cards' : 'Cards'; extraHeader += ` `; this.browser = new GenPageBrowserClass(container, this.listModelFolderAndFiles.bind(this), id, format, this.describeModel.bind(this), this.selectModel.bind(this), extraHeader); - this.browser.enableBrowserMultiSelect = true; this.promptBox = getRequiredElementById('alt_prompt_textbox'); this.models = {}; this.browser.refreshHandler = (callback) => { @@ -718,7 +717,7 @@ class ModelBrowserWrapper { }, can_multi: true }]; } let isStarred = this.isStarred(model.data.name); - let starButton = { label: isStarred ? 'Unstar' : 'Star', onclick: () => { this.toggleStar(model.data.name); }, can_multi: true }; + let starButton = { label: isStarred ? 'Unstar' : 'Star', onclick: () => { this.toggleStar(model.data.name); } }; buttons.push(starButton); let name = cleanModelName(model.data.name); let display = (model.data.display || name).replaceAll('/', ' / '); diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index 18c2840b4..f11bd6d45 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -72,30 +72,7 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { className: (metadata && metaParsed.is_starred) ? ' star-button button-starred-image' : ' star-button', onclick: (e) => { toggleStar(fullsrc, src); - }, - can_multi: true - }); - buttons.push({ - label: 'Enable Starred', - title: 'Marks all selected images as starred if they are not already', - onclick: (e) => { - if (!metaParsed.is_starred) { - toggleStar(fullsrc, src); - } - }, - can_multi: true, - multi_only: true - }); - buttons.push({ - label: 'Disabled Starred', - title: 'Marks all selected images as NOT starred if they are currently starred', - onclick: (e) => { - if (metaParsed.is_starred) { - toggleStar(fullsrc, src); - } - }, - can_multi: true, - multi_only: true + } }); } if (metadata) { @@ -168,11 +145,6 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { if (div) { removeImageBlockFromBatch(div); } - if (imageHistoryBrowser.enableBrowserMultiSelect) { - imageHistoryBrowser.multiSelectedKeys.delete(fullsrc); - imageHistoryBrowser.applyBrowserMultiSelectVisuals(); - imageHistoryBrowser.syncBrowserMultiSelectHeader(); - } }); }, // TODO: Only ask once for the multi-set rather than once per each @@ -254,62 +226,7 @@ function selectOutputInHistory(image, div) { let imageHistoryBrowser = new GenPageBrowserClass('image_history', listOutputHistoryFolderAndFiles, 'imagehistorybrowser', 'Thumbnails', describeOutputFile, selectOutputInHistory, ` `); -imageHistoryBrowser.enableBrowserMultiSelect = true; -imageHistoryBrowser.keepBrowserMultiSelectKeyAfterPrune = function(key, namesInCurrentList) { - if (namesInCurrentList.has(key)) { - return true; - } - let currentImageBatchDiv = getRequiredElementById('current_image_batch'); - for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { - if (getImageFullSrc(candidate.dataset.src) == key) { - return true; - } - } - return false; -}; -imageHistoryBrowser.applyExtraMultiSelectVisuals = function() { - let currentImageBatchDiv = getRequiredElementById('current_image_batch'); - for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { - let blockKey = getImageFullSrc(candidate.dataset.src); - if (!blockKey) { - continue; - } - let on = this.multiSelectActive && this.multiSelectedKeys.has(blockKey); - candidate.classList.toggle('browser-multiselect-entry-selected', on); - } -}; - -imageHistoryBrowser.getExtraMultiSelectedFiles = function(seen) { - let out = []; - let currentImageBatchDiv = getRequiredElementById('current_image_batch'); - for (let key of this.multiSelectedKeys) { - if (seen.has(key)) { - continue; - } - seen.add(key); - let block = null; - for (let candidate of currentImageBatchDiv.querySelectorAll('.image-block:not(.image-block-placeholder)')) { - if (getImageFullSrc(candidate.dataset.src) == key) { - block = candidate; - break; - } - } - let slash = key.lastIndexOf('/'); - let baseName = slash >= 0 ? key.substring(slash + 1) : key; - let src = block && block.dataset.src ? block.dataset.src : `${getImageOutPrefix()}/${key}`; - let metadata = block && block.dataset.metadata ? block.dataset.metadata : '{}'; - out.push({ - name: key, - data: { - src, - fullsrc: key, - name: baseName, - metadata - } - }); - } - return out; -}; +imageHistoryBrowser.allowMultiSelect = true; function storeImageToHistoryWithCurrentParams(img) { let data = getGenInput(); diff --git a/src/wwwroot/js/genpage/gentab/presets.js b/src/wwwroot/js/genpage/gentab/presets.js index 30f338ff2..9daf9892b 100644 --- a/src/wwwroot/js/genpage/gentab/presets.js +++ b/src/wwwroot/js/genpage/gentab/presets.js @@ -626,18 +626,18 @@ function listPresetFolderAndFiles(path, isRefresh, callback, depth) { function describePreset(preset) { let buttons = [ { label: 'Toggle', onclick: () => selectPreset(preset) }, - { label: 'Direct Apply', onclick: () => applyOnePreset(preset.data), can_multi: true }, - { label: preset.data.is_starred ? 'Unstar' : 'Star', onclick: () => togglePresetStar(preset), can_multi: true }, + { label: 'Direct Apply', onclick: () => applyOnePreset(preset.data) }, + { label: preset.data.is_starred ? 'Unstar' : 'Star', onclick: () => togglePresetStar(preset) }, { label: 'Edit Preset', onclick: () => editPreset(preset.data) }, { label: 'Duplicate Preset', onclick: () => duplicatePreset(preset.data) }, - { label: 'Export Preset', onclick: () => exportOnePresetButton(preset.data), can_multi: true }, + { label: 'Export Preset', onclick: () => exportOnePresetButton(preset.data) }, { label: 'Delete Preset', onclick: () => { if (confirm("Are you sure want to delete that preset?")) { genericRequest('DeletePreset', { preset: preset.data.title }, data => { loadUserData(); }); } - }, can_multi: true } + } } ]; let paramText = Object.keys(preset.data.param_map).map(key => `${key}: ${preset.data.param_map[key]}`); let description = `${preset.data.title}:\n${preset.data.description}\n\n${paramText.join('\n')}`; @@ -696,7 +696,6 @@ let presetBrowser = new GenPageBrowserClass('preset_list', listPresetFolderAndFi `); -presetBrowser.enableBrowserMultiSelect = true; function importPresetsButton() { getRequiredElementById('import_presets_textarea').value = ''; diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index 96676556a..b7ac045d8 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -107,9 +107,8 @@ class GenPageBrowserClass { this.runAfterUpdate = []; this.refreshHandler = (callback) => callback(); this.checkIsSmall(); - this.enableBrowserMultiSelect = false; + this.allowMultiSelect = false; this.multiSelectActive = false; - this.multiSelectedKeys = new Set(); this.multiSelectToggleButton = null; this.multiSelectActionSelect = null; } @@ -143,10 +142,7 @@ class GenPageBrowserClass { this.chunksRendered = 0; this.folder = folder; this.selected = null; - if (this.enableBrowserMultiSelect) { - this.multiSelectedKeys.clear(); - this.syncBrowserMultiSelectHeader(); - } + this.clearMultiSelection(); this.update(false, callback); } @@ -464,7 +460,7 @@ class GenPageBrowserClass { } let img = document.createElement('img'); img.addEventListener('click', () => { - if (this.handleBrowserMultiSelectTileClick(file, div)) { + if (this.handleMultiSelectTileClick(div)) { return; } this.select(file, div); @@ -478,21 +474,13 @@ class GenPageBrowserClass { let textBlock = createDiv(null, 'model-descblock'); textBlock.tabIndex = 0; textBlock.innerHTML = desc.description; - div.appendChild(textBlock); - div.addEventListener('click', (e) => { - if (!this.enableBrowserMultiSelect || !this.multiSelectActive) { - return; - } - if (e.target.closest('.model-block-menu-button')) { - return; - } - if (e.target.closest('img.image-block-img-inner')) { + textBlock.addEventListener('click', (e) => { + if (this.handleMultiSelectTileClick(div, e)) { return; } - e.preventDefault(); - e.stopPropagation(); - this.toggleBrowserMultiSelectForFile(file, div); + this.select(file, div); }); + div.appendChild(textBlock); } else if (this.format.includes('Thumbnails')) { div.className += ' image-block image-block-legacy'; @@ -522,7 +510,7 @@ class GenPageBrowserClass { textBlock.classList.add('image-preview-text-large'); } textBlock.addEventListener('click', (e) => { - if (this.handleBrowserMultiSelectTileClick(file, div, e)) { + if (this.handleMultiSelectTileClick(div, e)) { return; } this.select(file, div); @@ -534,7 +522,7 @@ class GenPageBrowserClass { let textBlock = createSpan(null, 'browser-list-entry-text'); textBlock.innerText = desc.display || desc.name; textBlock.addEventListener('click', () => { - if (this.handleBrowserMultiSelectTileClick(file, div)) { + if (this.handleMultiSelectTileClick(div)) { return; } this.select(file, div); @@ -555,7 +543,7 @@ class GenPageBrowserClass { textBlock.style.width = `calc(${percent}% - ${imgAdj}rem)`; textBlock.innerHTML = detail; textBlock.addEventListener('click', () => { - if (this.handleBrowserMultiSelectTileClick(file, div)) { + if (this.handleMultiSelectTileClick(div)) { return; } this.select(file, div); @@ -742,7 +730,7 @@ class GenPageBrowserClass { this.headerBar.appendChild(formatSelector); this.headerBar.appendChild(buttons); refreshButton.onclick = this.refresh.bind(this); - if (this.enableBrowserMultiSelect) { + if (this.allowMultiSelect) { this.multiSelectToggleButton = document.createElement('button'); this.multiSelectToggleButton.type = 'button'; this.multiSelectToggleButton.id = `${this.id}_multiselect_toggle`; @@ -750,7 +738,7 @@ class GenPageBrowserClass { this.multiSelectToggleButton.title = 'Toggle multi-select mode'; this.multiSelectToggleButton.innerHTML = '✓'; this.multiSelectToggleButton.addEventListener('click', () => { - this.setBrowserMultiSelectActive(!this.multiSelectActive); + this.setMultiSelectActive(!this.multiSelectActive); }); this.multiSelectActionSelect = document.createElement('select'); this.multiSelectActionSelect.id = `${this.id}_multiselect_action`; @@ -767,7 +755,7 @@ class GenPageBrowserClass { if (!choice) { return; } - this.runBrowserMultiSelectAction(choice); + this.runMultiSelectAction(choice); this.multiSelectActionSelect.value = ''; }); this.upButton.insertAdjacentElement('afterend', this.multiSelectToggleButton); @@ -837,10 +825,9 @@ class GenPageBrowserClass { this.buildTreeElements(this.folderTreeDiv, '', this.tree); applyTranslations(this.headerBar); if (!this.noContentUpdates) { - this.pruneBrowserMultiSelectionToCurrentList(); this.buildContentList(this.contentDiv, files); - this.applyBrowserMultiSelectVisuals(); - this.syncBrowserMultiSelectHeader(); + this.applyMultiSelectVisuals(); + this.syncMultiSelectHeader(); browserUtil.makeVisible(this.contentDiv); if (scrollOffset) { this.contentDiv.scrollTop = scrollOffset; @@ -857,62 +844,64 @@ class GenPageBrowserClass { } /** - * Removes all browser multi-select keys. + * Returns multi-select items. */ - clearBrowserMultiSelection() { - this.multiSelectedKeys.clear(); - this.syncBrowserMultiSelectHeader(); - this.applyBrowserMultiSelectVisuals(); + getMultiSelectedItems() { + if (!this.contentDiv) { + return []; + } + let items = []; + for (let child of this.contentDiv.children) { + if (child.dataset && child.dataset.name && child.classList.contains('browser-multiselect-item-selected')) { + items.push(child); + } + } + return items; } /** - * Turns browser multi-select mode on or off; exiting clears the selection. + * Clears multi-selected items. */ - setBrowserMultiSelectActive(active) { - if (!this.enableBrowserMultiSelect || this.multiSelectActive == active) { + clearMultiSelection() { + if (!this.allowMultiSelect) { return; } - this.multiSelectActive = active; - if (!active) { - this.multiSelectedKeys.clear(); + for (let item of this.getMultiSelectedItems()) { + item.classList.remove('browser-multiselect-item-selected'); } - this.syncBrowserMultiSelectHeader(); - this.applyBrowserMultiSelectVisuals(); + this.syncMultiSelectHeader(); } /** - * Toggles whether a file row is selected for bulk actions. + * Turns multi-select mode on or off; exiting clears the selection. */ - toggleBrowserMultiSelectForKey(key) { - if (!key) { + setMultiSelectActive(active) { + if (!this.allowMultiSelect || this.multiSelectActive == active) { return; } - if (this.multiSelectedKeys.has(key)) { - this.multiSelectedKeys.delete(key); + this.multiSelectActive = active; + if (!active) { + this.clearMultiSelection(); } else { - this.multiSelectedKeys.add(key); + this.syncMultiSelectHeader(); } - this.applyBrowserMultiSelectVisuals(); - this.syncBrowserMultiSelectHeader(); + this.applyMultiSelectVisuals(); } /** - * Toggles whether a file row is selected for bulk actions. + * Handles an item click while multi-select mode is active. */ - toggleBrowserMultiSelectForFile(file, div) { - this.toggleBrowserMultiSelectForKey(file.name); - } - - handleBrowserMultiSelectTileClick(file, div, event = null) { - if (!this.multiSelectActive || !this.enableBrowserMultiSelect) { + handleMultiSelectTileClick(div, event = null) { + if (!this.multiSelectActive || !this.allowMultiSelect) { return false; } if (event) { event.preventDefault(); event.stopPropagation(); } - this.toggleBrowserMultiSelectForFile(file, div); + div.classList.toggle('browser-multiselect-item-selected'); + this.syncMultiSelectHeader(); return true; } @@ -920,48 +909,25 @@ class GenPageBrowserClass { * Returns files in the current listing that are multi-selected. */ getMultiSelectedFiles() { - let seen = new Set(); - let out = []; - if (this.lastFiles) { - for (let file of this.lastFiles) { - if (this.multiSelectedKeys.has(file.name)) { - out.push(file); - seen.add(file.name); - } - } - } - return out.concat(this.getExtraMultiSelectedFiles(seen)); - } - - /** - * Returns true if this multi-selected key should remain after pruning. - */ - keepBrowserMultiSelectKeyAfterPrune(key, namesInCurrentList) { - return namesInCurrentList.has(key); - } - - /** - * Removes multi-selected keys for which keepBrowserMultiSelectKeyAfterPrune returns false. - */ - pruneBrowserMultiSelectionToCurrentList() { if (!this.lastFiles) { - return; + return []; } - let names = new Set(this.lastFiles.map(f => f.name)); - for (let key of [...this.multiSelectedKeys]) { - if (!this.keepBrowserMultiSelectKeyAfterPrune(key, names)) { - this.multiSelectedKeys.delete(key); + let selectedNames = new Set(this.getMultiSelectedItems().map(entry => entry.dataset.name)); + let out = []; + for (let file of this.lastFiles) { + if (selectedNames.has(file.name)) { + out.push(file); } } + return out; } /** * Labels for bulk actions shared by every selected item, respecting `can_multi` / `multi_only`. */ - collectCommonBulkActionLabels() { + getCommonMultiSelectActionLabels() { let files = this.getMultiSelectedFiles(); - let selCount = files.length; - if (selCount == 0) { + if (files.length == 0) { return []; } let eligiblePerFile = []; @@ -972,13 +938,13 @@ class GenPageBrowserClass { if (!button.onclick) { continue; } - if (button.multi_only && selCount < 2) { + if (button.multi_only && files.length < 2) { continue; } if (!button.can_multi && !button.multi_only) { continue; } - if (button.max_selected != null && selCount > button.max_selected) { + if (button.max_selected != null && files.length > button.max_selected) { continue; } labels.add(button.label); @@ -1000,7 +966,7 @@ class GenPageBrowserClass { * Off: ✓ ✓ * On: ☑ ☑ */ - syncBrowserMultiSelectToggleAppearance() { + syncMultiSelectToggleAppearance() { if (!this.multiSelectToggleButton) { return; } @@ -1009,14 +975,14 @@ class GenPageBrowserClass { } /** - * Updates multi-select toggle state and bulk action dropdown. + * Updates multi-select toggle state and action dropdown. */ - syncBrowserMultiSelectHeader() { - this.syncBrowserMultiSelectToggleAppearance(); + syncMultiSelectHeader() { + this.syncMultiSelectToggleAppearance(); if (!this.multiSelectActionSelect) { return; } - let show = this.multiSelectActive && this.multiSelectedKeys.size > 0; + let show = this.multiSelectActive && this.getMultiSelectedItems().length > 0; this.multiSelectActionSelect.style.display = show ? '' : 'none'; if (!show) { return; @@ -1024,8 +990,8 @@ class GenPageBrowserClass { while (this.multiSelectActionSelect.options.length > 1) { this.multiSelectActionSelect.remove(1); } - let labels = this.collectCommonBulkActionLabels(); - for (let label of labels) { + this.multiSelectActionSelect.value = ''; + for (let label of this.getCommonMultiSelectActionLabels()) { let opt = document.createElement('option'); opt.value = label; opt.className = 'translate'; @@ -1036,9 +1002,9 @@ class GenPageBrowserClass { } /** - * Runs a named bulk action (card popover label) once per selected item. + * Runs a multi-select action once per selected item. */ - runBrowserMultiSelectAction(label) { + runMultiSelectAction(label) { let files = this.getMultiSelectedFiles(); let failed = 0; for (let file of files) { @@ -1070,39 +1036,28 @@ class GenPageBrowserClass { } } if (failed > 0) { - showError(`Bulk action finished: ${failed} of ${files.length} failed — see console for details.`); + showError(`Bulk action finished: ${failed} of ${files.length} failed - see console for details.`); + } + if (label == 'Delete') { + this.setMultiSelectActive(false); + } + else { + this.applyMultiSelectVisuals(); + this.syncMultiSelectHeader(); } - this.pruneBrowserMultiSelectionToCurrentList(); - this.applyBrowserMultiSelectVisuals(); - this.syncBrowserMultiSelectHeader(); - } - - /** - * Applies multi-select visuals outside the browser content list. - */ - applyExtraMultiSelectVisuals() { - } - - /** - * Returns extra multi-selected files outside the current browser listing. - */ - getExtraMultiSelectedFiles(seen) { - return []; } /** * Applies multi-select highlight classes to visible rows. */ - applyBrowserMultiSelectVisuals() { - if (this.contentDiv) { - for (let child of this.contentDiv.children) { - if (!child.dataset || !child.dataset.name) { - continue; - } - let on = this.multiSelectActive && this.multiSelectedKeys.has(child.dataset.name); - child.classList.toggle('browser-multiselect-entry-selected', on); + applyMultiSelectVisuals() { + if (this.multiSelectActive || !this.contentDiv) { + return; + } + for (let child of this.contentDiv.children) { + if (child.dataset && child.dataset.name) { + child.classList.remove('browser-multiselect-item-selected'); } } - this.applyExtraMultiSelectVisuals(); } } From 64bfc8c2119c75639330bb71eee408d339cacf8b Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Sat, 2 May 2026 10:51:59 -0500 Subject: [PATCH 08/10] Adding back multi-select starring; unstarring currently not working --- .../js/genpage/gentab/outputhistory.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index f11bd6d45..5251ca559 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -65,7 +65,7 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { let mediaType = getMediaType(src); buttons = []; if (permissions.hasPermission('user_star_images') && !isDataImage) { - let metaParsed = JSON.parse(metadata); + let metaParsed = metadata ? (JSON.parse(metadata) || {}) : {}; buttons.push({ label: (metadata && metaParsed.is_starred) ? 'Unstar' : 'Star', title: 'Star or unstar this image - starred images get moved to a separate folder and highlighted.', @@ -74,6 +74,28 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { toggleStar(fullsrc, src); } }); + buttons.push({ + label: 'Enable Starred', + title: 'Marks all selected images as starred if they are not already', + onclick: (e) => { + if (!metaParsed.is_starred) { + toggleStar(fullsrc, src); + } + }, + can_multi: true, + multi_only: true + }); + buttons.push({ + label: 'Disabled Starred', + title: 'Marks all selected images as NOT starred if they are currently starred', + onclick: (e) => { + if (metaParsed.is_starred) { + toggleStar(fullsrc, src); + } + }, + can_multi: true, + multi_only: true + }); } if (metadata) { buttons.push({ From 867ab5117f90d66dc88f6ac52b5fee2dc914bd41 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Sat, 2 May 2026 11:02:49 -0500 Subject: [PATCH 09/10] Fix stale metadata lookup; nonexistant metadata --- src/wwwroot/js/genpage/gentab/outputhistory.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/wwwroot/js/genpage/gentab/outputhistory.js b/src/wwwroot/js/genpage/gentab/outputhistory.js index 5251ca559..e0b2bd181 100644 --- a/src/wwwroot/js/genpage/gentab/outputhistory.js +++ b/src/wwwroot/js/genpage/gentab/outputhistory.js @@ -65,7 +65,15 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { let mediaType = getMediaType(src); buttons = []; if (permissions.hasPermission('user_star_images') && !isDataImage) { - let metaParsed = metadata ? (JSON.parse(metadata) || {}) : {}; + let getMeta = (metadata) => metadata ? (JSON.parse(metadata) || {}) : {}; + let metaParsed = getMeta(metadata); + let isStarred = (e) => { + let currentMeta = e && e.dataset ? getMeta(e.dataset.metadata) : {}; + if (Object.keys(currentMeta).length == 0) { + currentMeta = metaParsed; + } + return currentMeta.is_starred; + }; buttons.push({ label: (metadata && metaParsed.is_starred) ? 'Unstar' : 'Star', title: 'Star or unstar this image - starred images get moved to a separate folder and highlighted.', @@ -78,7 +86,7 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { label: 'Enable Starred', title: 'Marks all selected images as starred if they are not already', onclick: (e) => { - if (!metaParsed.is_starred) { + if (!isStarred(e)) { toggleStar(fullsrc, src); } }, @@ -86,10 +94,10 @@ function buttonsForImage(fullsrc, src, metadata, isCurrentImage = false) { multi_only: true }); buttons.push({ - label: 'Disabled Starred', + label: 'Disable Starred', title: 'Marks all selected images as NOT starred if they are currently starred', onclick: (e) => { - if (metaParsed.is_starred) { + if (isStarred(e)) { toggleStar(fullsrc, src); } }, From 9ee9b5e3adde7ce604a349ffb829c761bf3b5115 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Sun, 3 May 2026 17:17:50 -0500 Subject: [PATCH 10/10] querySelectorAll --- src/wwwroot/js/genpage/helpers/browsers.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/wwwroot/js/genpage/helpers/browsers.js b/src/wwwroot/js/genpage/helpers/browsers.js index b7ac045d8..c515729b9 100644 --- a/src/wwwroot/js/genpage/helpers/browsers.js +++ b/src/wwwroot/js/genpage/helpers/browsers.js @@ -850,13 +850,7 @@ class GenPageBrowserClass { if (!this.contentDiv) { return []; } - let items = []; - for (let child of this.contentDiv.children) { - if (child.dataset && child.dataset.name && child.classList.contains('browser-multiselect-item-selected')) { - items.push(child); - } - } - return items; + return [...this.contentDiv.querySelectorAll(':scope > .browser-multiselect-item-selected[data-name]')]; } /**