feat: add batch search cancellation and dynamic footer menu items
This commit is contained in:
@@ -2445,6 +2445,13 @@
|
||||
outline: none;
|
||||
background: rgba(255,255,255,0.07) !important;
|
||||
}
|
||||
#model-resolver-modal .mr-footer-menu .mr-footer-menu-danger {
|
||||
color: var(--mr-color-error-light, #f55);
|
||||
}
|
||||
#model-resolver-modal .mr-footer-menu .mr-footer-menu-danger:hover,
|
||||
#model-resolver-modal .mr-footer-menu .mr-footer-menu-danger:focus-visible {
|
||||
background: var(--mr-bg-error-10, rgba(255, 0, 0, 0.1)) !important;
|
||||
}
|
||||
.mr-footer-menu-divider {
|
||||
height: 1px;
|
||||
margin: 4px 2px;
|
||||
|
||||
@@ -576,8 +576,10 @@ export const queueMethods = {
|
||||
this.selectMenuButton.classList.toggle('mr-btn-is-disabled', totalCount === 0);
|
||||
}
|
||||
if (this.searchMenuButton) {
|
||||
this.searchMenuButton.textContent = this.batchSearchRunning ? 'Searching...' : 'Search';
|
||||
this.searchMenuButton.classList.toggle('mr-btn-is-disabled', totalCount === 0 || this.batchSearchRunning);
|
||||
this.searchMenuButton.textContent = this.batchSearchRunning
|
||||
? (this.batchSearchCancelRequested ? 'Stopping...' : 'Searching...')
|
||||
: 'Search';
|
||||
this.searchMenuButton.classList.toggle('mr-btn-is-disabled', totalCount === 0 && !this.batchSearchRunning);
|
||||
}
|
||||
if (this.downloadMenuButton) {
|
||||
const label = activeCount > 0 ? `Cancel Downloads (${activeCount})` : 'Download';
|
||||
@@ -595,6 +597,7 @@ export const queueMethods = {
|
||||
if (this.applyPendingBtn) {
|
||||
this.applyPendingBtn.textContent = `Apply Selected (${pendingCount})`;
|
||||
}
|
||||
this.updateFooterMenuItemVisibility?.();
|
||||
},
|
||||
|
||||
createFooterMenu(name, label, items = [], buttonClass = 'mr-btn-secondary') {
|
||||
@@ -605,9 +608,7 @@ export const queueMethods = {
|
||||
onclick: (event) => {
|
||||
event.stopPropagation();
|
||||
if (button.classList.contains('mr-btn-is-disabled')) {
|
||||
if (name === 'search' && this.batchSearchRunning) {
|
||||
this.showNotification('Batch search is already running.', 'info');
|
||||
} else if (name === 'download') {
|
||||
if (name === 'download') {
|
||||
this.showNotification('No missing models have downloadable sources yet.', 'info');
|
||||
} else {
|
||||
this.showNotification('No missing models available.', 'info');
|
||||
@@ -618,8 +619,9 @@ export const queueMethods = {
|
||||
}
|
||||
}, [$el("span", { textContent: label })]);
|
||||
|
||||
const menu = $el("div.mr-footer-menu", { role: 'menu' }, items.map(item => (
|
||||
item === 'divider'
|
||||
const menuChildren = items.map(item => {
|
||||
const isDivider = item === 'divider' || item?.type === 'divider';
|
||||
const element = isDivider
|
||||
? $el("div.mr-footer-menu-divider", {})
|
||||
: $el("button.mr-footer-menu-item", {
|
||||
type: 'button',
|
||||
@@ -628,8 +630,21 @@ export const queueMethods = {
|
||||
event.stopPropagation();
|
||||
item.action();
|
||||
}
|
||||
}, [$el("span", { textContent: item.label })])
|
||||
)));
|
||||
}, [$el("span", { textContent: item.label })]);
|
||||
|
||||
if (item?.className) {
|
||||
element.classList.add(...String(item.className).split(/\s+/).filter(Boolean));
|
||||
}
|
||||
if (typeof item?.visibleWhen === 'function') {
|
||||
if (!this.footerMenuItems) this.footerMenuItems = new Map();
|
||||
if (!this.footerMenuItems.has(name)) this.footerMenuItems.set(name, []);
|
||||
this.footerMenuItems.get(name).push({ element, visibleWhen: item.visibleWhen });
|
||||
element.style.display = item.visibleWhen() ? '' : 'none';
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
const menu = $el("div.mr-footer-menu", { role: 'menu' }, menuChildren);
|
||||
|
||||
this.footerMenuButtons.set(name, button);
|
||||
this.footerMenus.set(name, menu);
|
||||
@@ -637,9 +652,19 @@ export const queueMethods = {
|
||||
return $el("div.mr-footer-menu-wrap", {}, [button, menu]);
|
||||
},
|
||||
|
||||
updateFooterMenuItemVisibility() {
|
||||
if (!this.footerMenuItems) return;
|
||||
for (const entries of this.footerMenuItems.values()) {
|
||||
for (const entry of entries) {
|
||||
entry.element.style.display = entry.visibleWhen() ? '' : 'none';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createFooter() {
|
||||
this.footerMenus = new Map();
|
||||
this.footerMenuButtons = new Map();
|
||||
this.footerMenuItems = new Map();
|
||||
|
||||
const selectMenu = this.createFooterMenu('select', 'Select (0/0)', [
|
||||
{ label: 'Select All', action: () => this.selectBatchMissingModels('all') },
|
||||
@@ -661,12 +686,14 @@ export const queueMethods = {
|
||||
this.selectMenuWrap = selectMenu;
|
||||
|
||||
const searchMenu = this.createFooterMenu('search', 'Search', [
|
||||
{ label: 'Search Selected', action: () => this.searchMissingBatch('selected', 'all') },
|
||||
{ label: 'Search All Missing', action: () => this.searchMissingBatch('all', 'all') },
|
||||
{ label: 'Search Unsearched', action: () => this.searchMissingBatch('unsearched', 'all') },
|
||||
'divider',
|
||||
{ label: 'Selected: CivitAI', action: () => this.searchMissingBatch('selected', 'civitai') },
|
||||
{ label: 'Selected: HuggingFace', action: () => this.searchMissingBatch('selected', 'huggingface') }
|
||||
{ label: 'Stop Searching', className: 'mr-footer-menu-danger', visibleWhen: () => this.batchSearchRunning, action: () => this.stopBatchSearch() },
|
||||
{ type: 'divider', visibleWhen: () => this.batchSearchRunning },
|
||||
{ label: 'Search Selected', visibleWhen: () => !this.batchSearchRunning, action: () => this.searchMissingBatch('selected', 'all') },
|
||||
{ label: 'Search All Missing', visibleWhen: () => !this.batchSearchRunning, action: () => this.searchMissingBatch('all', 'all') },
|
||||
{ label: 'Search Unsearched', visibleWhen: () => !this.batchSearchRunning, action: () => this.searchMissingBatch('unsearched', 'all') },
|
||||
{ type: 'divider', visibleWhen: () => !this.batchSearchRunning },
|
||||
{ label: 'Selected: CivitAI', visibleWhen: () => !this.batchSearchRunning, action: () => this.searchMissingBatch('selected', 'civitai') },
|
||||
{ label: 'Selected: HuggingFace', visibleWhen: () => !this.batchSearchRunning, action: () => this.searchMissingBatch('selected', 'huggingface') }
|
||||
]);
|
||||
this.searchMenuButton = this.footerMenuButtons.get('search');
|
||||
this.searchMenuWrap = searchMenu;
|
||||
|
||||
@@ -325,7 +325,7 @@ export const resolveDownloadMethods = {
|
||||
|
||||
async searchMissingBatch(mode = 'selected', source = 'all') {
|
||||
if (this.batchSearchRunning) {
|
||||
this.showNotification('Batch search is already running.', 'info');
|
||||
this.stopBatchSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -337,6 +337,7 @@ export const resolveDownloadMethods = {
|
||||
}
|
||||
|
||||
this.batchSearchRunning = true;
|
||||
this.batchSearchCancelRequested = false;
|
||||
this.updateBatchFooterButtons();
|
||||
this.closeFooterMenus();
|
||||
this.showNotification(`Searching ${targets.length} missing model${targets.length > 1 ? 's' : ''}...`, 'info');
|
||||
@@ -346,6 +347,9 @@ export const resolveDownloadMethods = {
|
||||
let failed = 0;
|
||||
try {
|
||||
for (const missing of targets) {
|
||||
if (this.batchSearchCancelRequested) {
|
||||
break;
|
||||
}
|
||||
const state = this.getSearchStateForWorkflow(batchWorkflowKey, missing);
|
||||
state.selectedSource = source || 'all';
|
||||
try {
|
||||
@@ -358,15 +362,34 @@ export const resolveDownloadMethods = {
|
||||
this.updateBatchFooterButtons();
|
||||
}
|
||||
} finally {
|
||||
const cancelled = this.batchSearchCancelRequested;
|
||||
this.batchSearchRunning = false;
|
||||
this.batchSearchCancelRequested = false;
|
||||
this.persistSearchStateForActiveWorkflow();
|
||||
this.updateBatchFooterButtons();
|
||||
const suffix = failed ? `, ${failed} failed` : '';
|
||||
if (cancelled) {
|
||||
this.showNotification(`Stopped search after ${completed} of ${targets.length} model${targets.length === 1 ? '' : 's'}${suffix}.`, 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const suffix = failed ? `, ${failed} failed` : '';
|
||||
this.showNotification(`Finished search for ${completed} model${completed === 1 ? '' : 's'}${suffix}.`, failed ? 'error' : 'success');
|
||||
},
|
||||
|
||||
stopBatchSearch() {
|
||||
if (!this.batchSearchRunning) {
|
||||
this.closeFooterMenus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.batchSearchCancelRequested = true;
|
||||
this.closeFooterMenus();
|
||||
this.updateBatchFooterButtons();
|
||||
this.showNotification('Stopping batch search after the current model...', 'info');
|
||||
},
|
||||
|
||||
async downloadMissingBatch(mode = 'selected') {
|
||||
const targets = mode === 'selected'
|
||||
? this.getSelectedMissingModels()
|
||||
|
||||
@@ -43,6 +43,7 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
this.lastBatchSelectedMissingKey = null;
|
||||
this.activeFooterMenu = null;
|
||||
this.batchSearchRunning = false;
|
||||
this.batchSearchCancelRequested = false;
|
||||
this.boundHandleOutsideClick = this.handleOutsideClick.bind(this);
|
||||
this.activeTabStorageKey = 'model_resolver_active_tab';
|
||||
this.activeTab = this.restoreActiveTab(); // Default tab
|
||||
|
||||
Reference in New Issue
Block a user