diff --git a/CHANGELOG.md b/CHANGELOG.md index c672b39..0605388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - **Before an online scan, the plugin checks whether Civitai can be reached**, on both its addresses. When it cannot, it asks instead of waiting on every model: **Scan from the files** (base models found, no covers or trigger words; a later scan fills them in), **Set a proxy** (opens Settings → Models → Network), or Cancel. A check that reached Civitai is trusted for five minutes. - **Civitai's other address (civitai.red) when civitai.com does not answer**: some networks reach only the second, a cloud server in mainland China among them (tested on AutoDL: civitai.com timed out, civitai.red answered in about a second). Scans, import and Model Check's lookups and **Settings → Network → Test** ask civitai.red then, so models still get their names, trigger words and base models; the test says which address answered. The covers are on Civitai's picture server, which such networks often cannot reach either: after two covers in a row that would not download, a scan fetches no more covers instead of waiting on each. +### 🧩 Combos (搭配) +- **A combo's models are cover cards**, as in the model library: the cover on top, the name below. In a combo saved from picked nodes, the slot (Main model, LoRA…) is written on the cover and a click on the card changes the model; in a default combo, the main models and LoRAs to pick from are cover cards side by side. They used to be rows as wide as the window, with **Change** far from the name (模型卡片不再横向拉满整个窗口). + ### ☁️ ComfyUI on a cloud server (云服务器上使用) - **How far it goes**: fully supported on overseas cloud servers (RunPod…). On servers in mainland China (AutoDL…) only in part: browsing, offline scans and tidying work, but Civitai is mostly out of reach there, so lookups are unreliable and covers rarely download (海外服务器完整支持;国内服务器只能部分支持). Not fully tested yet: one AutoDL server so far, no overseas one. The cloud guide ends with a section for AI assistants (what is going on, the steps, how to check, what to do when it did not work, what to tell the user about safety), so a user can hand the link to their AI. - On a rented cloud server (AutoDL, RunPod…) the page is always opened from another device, so since v1.58.1 the plugin only let it look: scans, downloads, moves and saves were refused. Now there are two ways: open ComfyUI through an **SSH tunnel** (the address starts with 127.0.0.1; it counts as the computer ComfyUI runs on, nothing to change), or make an empty file named **`allow_remote_changes`** in the plugin folder on the server and restart ComfyUI. With the file, every device that can open the page may change things; the page can neither make nor remove it, and the MCP endpoint stays for this computer only. How, and the risk: [docs/guides/cloud-server.md](docs/guides/cloud-server.md). diff --git a/web/modules/ui_combo_structure.js b/web/modules/ui_combo_structure.js index ccedf43..6a47cc7 100644 --- a/web/modules/ui_combo_structure.js +++ b/web/modules/ui_combo_structure.js @@ -361,12 +361,18 @@ export function renderStructuredCombo(owner, host, note, toolbar) { return [slot.id, slotLabel({ ...slot, index: counts[slot.folder] > 1 ? seen[slot.folder] : 0 })]; })); // Which node a slot is on, and its node pack when it is not ComfyUI's own. + const nodeName = (slot) => { + const node = nodeOf.get(slot.node); + if (!node) return null; + return { text: `${node.title || node.name || node.type}${node.pack ? `(${node.pack})` : ''}`, + title: node.pack ? `${node.type} · ${node.pack}` : node.type }; + }; const labelLine = (slot) => { const line = el('span', 'anomalous-structure-label', labels.get(slot.id)); - const node = nodeOf.get(slot.node); + const node = nodeName(slot); if (node) { - line.append(el('span', 'anomalous-structure-node', ` · ${node.title || node.name || node.type}${node.pack ? `(${node.pack})` : ''}`)); - line.title = node.pack ? `${node.type} · ${node.pack}` : node.type; + line.append(el('span', 'anomalous-structure-node', ` · ${node.text}`)); + line.title = node.title; } return line; }; @@ -392,34 +398,43 @@ export function renderStructuredCombo(owner, host, note, toolbar) { save(true); row.replaceWith(drawModel(slot)); }; + // A model is a cover card, like the model library's: its slot on the cover, a click on it changes the model. const drawModel = (slot) => { const row = el('div', 'anomalous-structure-row is-model'); row.dataset.slot = slot.id; const value = data.values[slot.id]; + const cover = el('div', 'anomalous-structure-cover'); + cover.append(drawThumb(slot), el('span', 'anomalous-structure-badge', labels.get(slot.id))); const copy = el('div', 'anomalous-structure-copy'); - copy.append(labelLine(slot), - el('strong', 'anomalous-structure-value', isNone(value) ? t('comboSlotEmpty') : shortName(value))); + copy.append(el('strong', 'anomalous-structure-value', isNone(value) ? t('comboSlotEmpty') : shortName(value))); if (!isNone(value)) copy.title = value; + const node = nodeName(slot); + if (node) { + const line = el('span', 'anomalous-structure-node', node.text); + line.title = node.title; + copy.append(line); + } // The node's other values, as saved: shown, not changed here (a combo puts down exactly what was saved). const fixed = fixedValues(nodeOf.get(slot.node), structure.slots, slot); if (fixed.length) copy.append(chipLine(fixed)); - const actions = el('div', 'anomalous-structure-actions'); if (slot.folder) { - actions.append(button('anomalous-scan-row-btn is-main', t('comboPick'), () => pick(slot, row))); + cover.append(button('anomalous-structure-change', t('comboPick'), () => pick(slot, row))); row.classList.add('is-pickable'); row.onclick = (event) => { if (!event.target.closest('button')) void pick(slot, row); }; } else { copy.append(el('span', 'anomalous-structure-note is-muted', t('comboSlotUnknownFolder'))); } if (slot.optional && !isNone(value)) { + const actions = el('div', 'anomalous-structure-actions'); actions.append(button('anomalous-scan-row-btn', t('comboSlotClear'), () => { data.values[slot.id] = 'None'; delete data.covers[slot.id]; save(true); row.replaceWith(drawModel(slot)); })); + copy.append(actions); } - row.append(drawThumb(slot), copy, actions); + row.append(cover, copy); return row; }; const fits = []; @@ -459,7 +474,13 @@ export function renderStructuredCombo(owner, host, note, toolbar) { const section = el('div', 'anomalous-structure-group'); const heading = el('h4', 'anomalous-structure-heading', t(key)); if (slots.length > 1) heading.append(el('span', 'anomalous-structure-count', String(slots.length))); - section.append(heading, ...slots.map(slot => (slot.kind === 'text' ? drawText(slot) : drawModel(slot)))); + if (key === 'comboGroupPrompts') { + section.append(heading, ...slots.map(drawText)); + } else { + const covers = el('div', 'anomalous-structure-models'); + covers.append(...slots.map(drawModel)); + section.append(heading, covers); + } card.append(section); } // Nodes without slots go down exactly as saved: their name and saved values, nothing to change here. diff --git a/web/modules/ui_notebook_editor.js b/web/modules/ui_notebook_editor.js index f720aac..d442c7a 100644 --- a/web/modules/ui_notebook_editor.js +++ b/web/modules/ui_notebook_editor.js @@ -511,7 +511,7 @@ export function fillNotebookGalleries(baseModel, mainGallery, loraGallery, data) if (isVid) return ``; return ``; } - return `
?
`; + return '
?
'; }; fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=checkpoints,unet,diffusion_models`) diff --git a/web/styles.css b/web/styles.css index e27cad8..5a40bb7 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1,6 +1,6 @@ @import url("./styles/00-foundation-models.css?v=20261014-folders-1"); @import url("./styles/01-shell-tools.css?v=20261014-folders-1"); -@import url("./styles/02-notebooks.css?v=20261014-loranode-1"); +@import url("./styles/02-notebooks.css?v=20261017-covers"); @import url("./styles/03-gallery-recipes.css?v=20261014-file-2"); @import url("./styles/04-dialogs-materials.css?v=20261011-wfd-3"); @import url("./styles/05-recipe-studio-overrides.css?v=20261011-wfd-3"); @@ -21,7 +21,7 @@ @import url("./styles/20-current-node.css?v=20261011-last"); @import url("./styles/21-prompt-studio.css?v=20261007-theme-2"); @import url("./styles/22-model-download.css?v=20261007-theme-2"); -@import url("./styles/23-combo-structure.css?v=20261007-theme-2"); +@import url("./styles/23-combo-structure.css?v=20261017-covers"); @import url("./styles/24-backup.css?v=20261007-theme-2"); @import url("./styles/25-model-import.css?v=20261008-import"); @import url("./styles/26-model-tidy.css?v=20261008-tidy"); diff --git a/web/styles/02-notebooks.css b/web/styles/02-notebooks.css index 6eefa54..e79a561 100644 --- a/web/styles/02-notebooks.css +++ b/web/styles/02-notebooks.css @@ -476,18 +476,17 @@ overflow: visible; } +/* Cover cards, as in the model library: the cover on top, the name below. */ .anomalous-nb-minicheck, .anomalous-nb-minilora { display: flex; - align-items: center; - gap: 12px; - padding: 8px 12px 8px 8px; + flex: 0 0 120px; + flex-direction: column; + width: 120px; background: var(--amb-bg-card-hover); border: 1px solid var(--amb-border); border-radius: 8px; cursor: pointer; - flex: 1 1 220px; - max-width: 320px; transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1); } @@ -510,15 +509,25 @@ .anomalous-nb-minicheck img, .anomalous-nb-minicheck video, .anomalous-nb-minilora img, -.anomalous-nb-minilora video { - width: 52px; - height: 52px; - aspect-ratio: 1 / 1; +.anomalous-nb-minilora video, +.anomalous-nb-mini-empty { + width: 100%; + height: auto; + aspect-ratio: 3 / 4; object-fit: cover; - border-radius: 6px; + border-radius: 7px 7px 0 0; flex-shrink: 0; } +.anomalous-nb-mini-empty { + display: flex; + align-items: center; + justify-content: center; + background: var(--amb-bg-card); + color: var(--amb-text-dim); + font-size: 12px; +} + .anomalous-nb-minicheck-name, .anomalous-nb-minilora-name { color: var(--amb-text-main); @@ -529,7 +538,10 @@ -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; + padding: 6px 8px 7px; + font-size: 12px; text-align: left; + word-break: break-word; } .anomalous-nb-minicheck.selected .anomalous-nb-minicheck-name, diff --git a/web/styles/23-combo-structure.css b/web/styles/23-combo-structure.css index 22ef035..269af52 100644 --- a/web/styles/23-combo-structure.css +++ b/web/styles/23-combo-structure.css @@ -204,7 +204,6 @@ line-height: 18px; } -.anomalous-structure-row.is-model, .anomalous-structure-row.is-fixed { display: flex; align-items: center; @@ -215,9 +214,81 @@ background: var(--amb-bg-card); } +/* Models: cover cards side by side, as in the model library. */ +.anomalous-structure-models { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.anomalous-structure-row.is-model { + display: flex; + flex-direction: column; + width: 150px; + overflow: hidden; + border: 1px solid var(--amb-border); + border-radius: 10px; + background: var(--amb-bg-card); +} + .anomalous-structure-row.is-pickable { cursor: pointer; } .anomalous-structure-row.is-pickable:hover { border-color: var(--amb-link); } +.anomalous-structure-cover { position: relative; } + +.anomalous-structure-row.is-model .anomalous-structure-thumb { + width: 100%; + height: auto; + aspect-ratio: 3 / 4; + border-radius: 0; +} + +/* Text on the cover: dark plates, readable on any picture and in either theme. */ +.anomalous-structure-badge { + position: absolute; + top: 6px; + left: 6px; + max-width: calc(100% - 12px); + overflow: hidden; + padding: 1px 7px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.62); + color: #fff; + font-size: 11px; + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.anomalous-structure-change { + position: absolute; + bottom: 8px; + left: 50%; + padding: 3px 14px; + border: 0; + border-radius: 999px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font: inherit; + font-size: 12px; + opacity: 0; + transform: translateX(-50%); + transition: opacity 0.15s; + cursor: pointer; +} + +.anomalous-structure-row.is-model:hover .anomalous-structure-change, +.anomalous-structure-change:focus-visible { opacity: 1; } + +@media (hover: none) { + .anomalous-structure-change { opacity: 1; } +} + +.anomalous-structure-row.is-model .anomalous-structure-copy { padding: 6px 8px 8px; } +.anomalous-structure-row.is-model .anomalous-structure-value { font-size: 13px; } +.anomalous-structure-row.is-model .anomalous-structure-node { font-size: 11px; } +.anomalous-structure-row.is-model .anomalous-structure-actions { margin-top: 4px; } + .anomalous-structure-row.is-text { display: flex; flex-direction: column;