diff --git a/web/css/linker-main.css b/web/css/linker-main.css index ba806ac..9a2ba65 100644 --- a/web/css/linker-main.css +++ b/web/css/linker-main.css @@ -648,8 +648,9 @@ /* Download Section */ .ml-download-section { + --ml-download-control-height: 36px; display: grid; - gap: var(--ml-spacing-8); + gap: 10px; padding: var(--ml-spacing-10); background: var(--ml-surface-elevated); border-radius: 0; @@ -673,39 +674,64 @@ margin-left: var(--ml-spacing-8); } .ml-search-source-bar { - display: flex; + display: grid; + grid-template-columns: auto minmax(178px, 1fr); align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 8px; + gap: 10px; + margin-bottom: 0; +} +.ml-download-section .ml-search-source-bar > .ml-btn { + height: var(--ml-download-control-height); + min-height: var(--ml-download-control-height); + padding: 0 12px; + border-radius: 9px; + font-size: 12px; + line-height: 1; } .ml-search-source-picker { - display: flex; + display: grid; + grid-template-columns: auto minmax(0, 1fr); align-items: center; - gap: 8px; + gap: 10px; min-width: 0; margin-left: auto; + width: 100%; } .ml-search-source-picker-label { color: var(--ml-text-muted); - font-size: 11px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; white-space: nowrap; } .ml-search-source-select { - min-height: 30px; - padding: 5px 10px; + width: 100%; + height: var(--ml-download-control-height); + min-height: var(--ml-download-control-height); + padding: 0 11px; background: rgba(255,255,255,0.04); color: var(--ml-text); border: 1px solid var(--ml-border); - border-radius: 10px; - font-size: 11px; + border-radius: 9px; + font-size: 12px; + line-height: 1; outline: none; appearance: none; + box-sizing: border-box; } .ml-search-source-select:focus { border-color: rgba(114,211,138,0.4); box-shadow: 0 0 0 3px rgba(114,211,138,0.12); } +@media (max-width: 720px) { + .ml-search-source-bar { + grid-template-columns: 1fr; + } + .ml-download-section .ml-search-source-bar > .ml-btn { + justify-self: start; + } +} .ml-search-results { margin-top: 8px; display: none; @@ -2260,7 +2286,7 @@ } } .ml-download-target { - margin-top: 10px; + margin-top: 0; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); display: grid; @@ -2270,7 +2296,7 @@ display: grid; grid-template-columns: minmax(120px, 33%) minmax(0, 1fr); grid-template-rows: auto auto; - gap: 10px; + gap: 8px 10px; align-items: start; } .ml-download-target-row { @@ -2297,15 +2323,16 @@ .ml-download-target-select { display: block; width: 100%; - height: 40px; - margin: 3px; - padding: 8px 10px; + height: var(--ml-download-control-height, 36px); + min-height: var(--ml-download-control-height, 36px); + margin: 0; + padding: 0 11px; background: rgba(255,255,255,0.04); color: var(--ml-text); border: 1px solid var(--ml-border); - border-radius: 10px; + border-radius: 9px; font-size: 12px; - line-height: 1.2; + line-height: 1; outline: none; box-sizing: border-box; } @@ -2313,6 +2340,10 @@ appearance: none; -webkit-appearance: none; -moz-appearance: none; + cursor: pointer; +} +.ml-search-source-select { + cursor: pointer; } .ml-download-target-input:focus, .ml-download-target-select:focus { @@ -2323,6 +2354,8 @@ position: relative; display: flex; align-items: stretch; + min-width: 0; + width: 100%; } @media (max-width: 720px) { .ml-download-target-label { @@ -2357,9 +2390,3 @@ .ml-download-target-option.is-highlighted { background: rgba(114,211,138,0.12); } -.ml-search-source-select option, -.ml-download-target-select option { - background: var(--ml-panel-bg-strong); - color: var(--ml-text); -} - diff --git a/web/linker.js b/web/linker.js index b438bae..136bffa 100644 --- a/web/linker.js +++ b/web/linker.js @@ -389,7 +389,7 @@ class LinkerManagerDialog extends ComfyDialog { const state = this.getSearchState(missing); const selectEl = container.querySelector(`#search-source-select-${missing.node_id}-${missing.widget_index}`); if (selectEl) { - selectEl.value = state.selectedSource; + this.setDropdownValue(selectEl, state.selectedSource, this.getSearchSourceLabel(state.selectedSource)); } } @@ -402,6 +402,16 @@ class LinkerManagerDialog extends ComfyDialog { this.syncSearchSourceUi(missing, container); } + getDropdownValue(el) { + return el?.dataset?.value || el?.value || ''; + } + + setDropdownValue(el, value, label = value) { + if (!el) return; + el.dataset.value = value || ''; + el.value = label || value || ''; + } + normalizeVersionName(versionName) { return String(versionName || '').trim().replace(/^v{2,}(?=\d)/i, 'v'); } @@ -693,7 +703,10 @@ class LinkerManagerDialog extends ComfyDialog { renderSearchControls(missing, options = {}) { const searchSourcesId = `search-sources-${missing.node_id}-${missing.widget_index}`; const searchSourceSelectId = `search-source-select-${missing.node_id}-${missing.widget_index}`; + const searchSourceListId = `search-source-list-${missing.node_id}-${missing.widget_index}`; const buttonText = options.buttonText || 'Search'; + const state = this.getSearchState(missing); + const selectedSource = state.selectedSource || 'all'; let html = `
`; html += ``; html += `
`; html += ``; - html += ``; + html += `
`; + html += ``; + html += `
`; + html += `
`; html += `
`; html += `
`; return html; @@ -2206,6 +2214,17 @@ class LinkerManagerDialog extends ComfyDialog { return ordered.length > 0 ? ordered : [preferred]; } + getSearchSourceOptions() { + const sources = ['all', 'local', 'huggingface', 'civitai']; + if (this.isSourceAvailable('lora_manager_archive')) { + sources.push('lora_manager_archive'); + } + return sources.map(source => ({ + value: source, + label: this.getSearchSourceLabel(source) + })); + } + getAvailableSubfolders(category = '') { return this.downloadSubfolders.get((category || '').toLowerCase()) || []; } @@ -2278,9 +2297,10 @@ class LinkerManagerDialog extends ComfyDialog { async applySuggestedCivitaiSubfolder(missing, categoryEl, subfolderEl) { if (!categoryEl || !subfolderEl || subfolderEl.value.trim()) return; - await this.ensureDownloadSubfoldersLoaded(categoryEl.value); - const folders = this.getAvailableSubfolders(categoryEl.value); - const suggestion = this.getSuggestedCivitaiSubfolder(missing, categoryEl.value, folders); + const category = this.getDropdownValue(categoryEl); + await this.ensureDownloadSubfoldersLoaded(category); + const folders = this.getAvailableSubfolders(category); + const suggestion = this.getSuggestedCivitaiSubfolder(missing, category, folders); if (suggestion) { subfolderEl.value = suggestion; } @@ -2321,16 +2341,18 @@ class LinkerManagerDialog extends ComfyDialog { renderDownloadTargetControls(missing, defaultCategory = 'checkpoints') { const selectId = `download-category-${missing.node_id}-${missing.widget_index}`; const subfolderId = `download-subfolder-${missing.node_id}-${missing.widget_index}`; + const categoryListId = `download-category-list-${missing.node_id}-${missing.widget_index}`; const subfolderListId = `download-subfolder-list-${missing.node_id}-${missing.widget_index}`; - const options = this.getDownloadCategoryOptions(defaultCategory) - .map(category => ``) - .join(''); + const selectedCategory = defaultCategory || 'checkpoints'; let html = `
`; html += `
`; html += ``; html += ``; - html += ``; + html += `
`; + html += ``; + html += `
`; + html += `
`; html += `
`; html += ``; html += `
`; @@ -2344,7 +2366,7 @@ class LinkerManagerDialog extends ComfyDialog { const categoryEl = this.contentElement?.querySelector(`#download-category-${missing.node_id}-${missing.widget_index}`); const subfolderEl = this.contentElement?.querySelector(`#download-subfolder-${missing.node_id}-${missing.widget_index}`); return { - category: categoryEl?.value || fallbackCategory || 'checkpoints', + category: this.getDropdownValue(categoryEl) || fallbackCategory || 'checkpoints', subfolder: (subfolderEl?.value || '').trim() }; } @@ -2379,21 +2401,34 @@ class LinkerManagerDialog extends ComfyDialog { wireDownloadTargetAutocomplete(container, missing) { const categoryEl = container.querySelector(`#download-category-${missing.node_id}-${missing.widget_index}`); const subfolderEl = container.querySelector(`#download-subfolder-${missing.node_id}-${missing.widget_index}`); + const categoryListEl = container.querySelector(`#download-category-list-${missing.node_id}-${missing.widget_index}`); const listEl = container.querySelector(`#download-subfolder-list-${missing.node_id}-${missing.widget_index}`); if (!categoryEl || !subfolderEl || !listEl) return; this.enableWheelScrollChaining(listEl); + if (categoryListEl) { + this.enableWheelScrollChaining(categoryListEl); + } const renderOptions = (targetEl, values, onSelect) => { - if (!values.length) { + const options = values.map(value => ( + typeof value === 'object' + ? value + : { value, label: value } + )); + if (!options.length) { targetEl.innerHTML = ''; targetEl.style.display = 'none'; return; } - targetEl.innerHTML = values + targetEl.innerHTML = options .slice(0, 50) - .map(value => `
${value}
`) + .map(option => { + const value = String(option.value || ''); + const label = String(option.label || value); + return `
${this.escapeHtml(label)}
`; + }) .join(''); targetEl.style.display = 'block'; @@ -2402,16 +2437,33 @@ class LinkerManagerDialog extends ComfyDialog { option.addEventListener('mousedown', (event) => { event.preventDefault(); const value = decodeURIComponent(option.dataset.value || ''); - onSelect(value); + const label = decodeURIComponent(option.dataset.label || option.dataset.value || ''); + onSelect(value, label); targetEl.style.display = 'none'; }); }); }; + const populateCategoryOptions = () => { + if (!categoryListEl) return; + const options = this.getDownloadCategoryOptions(this.getDropdownValue(categoryEl) || 'checkpoints') + .map(category => ({ + value: category, + label: this.getCategoryDisplayName(category) + })); + renderOptions(categoryListEl, options, (value, label) => { + this.setDropdownValue(categoryEl, value, label); + subfolderEl.value = ''; + populateSubfolderOptions(''); + this.applySuggestedCivitaiSubfolder(missing, categoryEl, subfolderEl); + }); + }; + const populateSubfolderOptions = async (filterText = '') => { const filter = (filterText || '').toLowerCase(); - await this.ensureDownloadSubfoldersLoaded(categoryEl.value); - const folders = this.getAvailableSubfolders(categoryEl.value); + const category = this.getDropdownValue(categoryEl); + await this.ensureDownloadSubfoldersLoaded(category); + const folders = this.getAvailableSubfolders(category); const filtered = filter ? folders.filter(folder => folder.toLowerCase().includes(filter)) : folders; @@ -2427,10 +2479,18 @@ class LinkerManagerDialog extends ComfyDialog { }, 150); }; - categoryEl.addEventListener('change', () => { - populateSubfolderOptions(subfolderEl.value); - this.applySuggestedCivitaiSubfolder(missing, categoryEl, subfolderEl); - }); + if (categoryListEl && categoryEl.dataset.mlCategoryBound !== 'true') { + categoryEl.dataset.mlCategoryBound = 'true'; + categoryEl.addEventListener('focus', () => populateCategoryOptions()); + categoryEl.addEventListener('click', () => populateCategoryOptions()); + categoryEl.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') { + event.preventDefault(); + populateCategoryOptions(); + } + }); + categoryEl.addEventListener('blur', () => hideList(categoryListEl)); + } subfolderEl.addEventListener('focus', () => { populateSubfolderOptions(subfolderEl.value); @@ -5194,11 +5254,44 @@ class LinkerManagerDialog extends ComfyDialog { } const sourceSelect = container.querySelector(`#search-source-select-${missing.node_id}-${missing.widget_index}`); - if (sourceSelect && sourceSelect.dataset.mlSearchSourceBound !== 'true') { + const sourceList = container.querySelector(`#search-source-list-${missing.node_id}-${missing.widget_index}`); + if (sourceSelect && sourceList && sourceSelect.dataset.mlSearchSourceBound !== 'true') { sourceSelect.dataset.mlSearchSourceBound = 'true'; - sourceSelect.addEventListener('change', () => { - this.setSearchSource(missing, sourceSelect.value, container); + + const renderSourceOptions = () => { + const options = this.getSearchSourceOptions(); + sourceList.innerHTML = options + .map(option => `
${this.escapeHtml(option.label)}
`) + .join(''); + sourceList.style.display = 'block'; + sourceList.querySelectorAll('.ml-download-target-option').forEach(optionEl => { + optionEl.addEventListener('mousedown', (event) => { + event.preventDefault(); + const value = decodeURIComponent(optionEl.dataset.value || ''); + const label = decodeURIComponent(optionEl.dataset.label || optionEl.dataset.value || ''); + this.setDropdownValue(sourceSelect, value, label); + this.setSearchSource(missing, value, container); + sourceList.style.display = 'none'; + }); + }); + }; + + const hideSourceList = () => { + setTimeout(() => { + sourceList.style.display = 'none'; + }, 150); + }; + + this.enableWheelScrollChaining(sourceList); + sourceSelect.addEventListener('focus', () => renderSourceOptions()); + sourceSelect.addEventListener('click', () => renderSourceOptions()); + sourceSelect.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') { + event.preventDefault(); + renderSourceOptions(); + } }); + sourceSelect.addEventListener('blur', hideSourceList); this.syncSearchSourceUi(missing, container); }