/** * ui_detail.js * Extracted Detail Panel methods. */ import { app } from "../../../scripts/app.js"; import { i18n } from './locales.js'; import { escapeHtml, setSafeRichHtml } from './safe_dom.js'; const t = (key) => { let lang = window.anomalous_browser_lang || 'zh'; if (lang.startsWith('en')) lang = 'en'; return (i18n[lang] && i18n[lang][key]) ? i18n[lang][key] : (i18n['zh'][key] || key); }; export function showDetail(model) { if (this.isPickingModelForNode) { const targetNode = this.isPickingModelForNode.node; const targetWidget = this.isPickingModelForNode.widget; const normVal = (model.filename).replace(/\\/g, '/'); let foundPath = model.filename; if (targetWidget.options && targetWidget.options.values) { const exactMatch = targetWidget.options.values.find(v => typeof v === 'string' && v.replace(/\\/g, '/').endsWith(normVal)); if (exactMatch) foundPath = exactMatch; } targetWidget.value = foundPath; delete targetNode.color; delete targetNode.bgcolor; targetNode.has_errors = false; if (targetWidget.callback) targetWidget.callback(targetWidget.value, app.canvas, targetNode, app.canvas.graph_mouse, null); app.graph.setDirtyCanvas(true, true); this.isPickingModelForNode = null; const banner = document.getElementById('anomalous-picker-banner'); if (banner) banner.remove(); if (this.grid) this.grid.style.display = 'none'; this.doctorPanel.style.display = 'flex'; this.diagnoseNode(targetNode); return; } if (this.currentDetailObserver) { this.currentDetailObserver.disconnect(); this.currentDetailObserver = null; } if (this.detailMouseMoveHandler) window.removeEventListener('mousemove', this.detailMouseMoveHandler); if (this.detailMouseUpHandler) window.removeEventListener('mouseup', this.detailMouseUpHandler); this.grid.style.display = 'none'; this.detailPanel.style.display = 'flex'; this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; const header = document.createElement('div'); header.style.width = '100%'; header.style.padding = '8px 15px'; header.style.background = 'var(--comfy-menu-bg, #333)'; header.style.borderBottom = '1px solid var(--border-color, #444)'; header.style.display = 'flex'; header.style.alignItems = 'center'; header.style.boxSizing = 'border-box'; const backBtn = document.createElement('button'); let isFromDoctor = false; let isFromAssistant = false; if (this.historyStack.length > 0) { const lastHistory = this.historyStack[this.historyStack.length - 1]; if (lastHistory.type === 'doctor') { isFromDoctor = true; } else if (lastHistory.type === 'assistant') { isFromAssistant = true; } } if (isFromDoctor) { backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回医生面板' : '⬅ Back to Doctor'; backBtn.style.background = '#8AB4F8'; backBtn.style.color = '#000'; } else if (isFromAssistant) { backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回助手面板' : '⬅ Back to Assistant'; backBtn.style.background = '#8AB4F8'; backBtn.style.color = '#000'; } else { backBtn.innerHTML = this.historyStack.length > 0 ? t('backToPrev') : t('back'); backBtn.style.background = '#444'; backBtn.style.color = '#fff'; } backBtn.style.padding = '6px 12px'; backBtn.style.border = 'none'; backBtn.style.borderRadius = '4px'; backBtn.style.cursor = 'pointer'; backBtn.onclick = () => { if (this.currentDetailObserver) { this.currentDetailObserver.disconnect(); this.currentDetailObserver = null; } if (this.historyStack.length > 0) { const prev = this.historyStack.pop(); if (prev.type === 'doctor') { this.detailPanel.style.display = 'none'; this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; this.doctorPanel.style.display = 'flex'; return; } if (prev.type === 'assistant') { this.detailPanel.style.display = 'none'; this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; this.assistantPanel.style.display = 'flex'; return; } this.currentType = prev.type; this.currentPathIdx = prev.pathIdx; this.currentSubfolder = prev.subfolder; this.currentDetailModel = prev.model; this.renderSidebar(); this.showDetail(prev.model); } else { this.detailPanel.style.display = 'none'; this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; this.grid.style.display = 'grid'; } }; const title = document.createElement('h2'); title.textContent = model.filename; title.style.margin = '0 20px 0 20px'; title.style.color = '#fff'; title.style.fontSize = '1.2em'; // 强制单行并溢出显示省略号 title.style.whiteSpace = 'nowrap'; title.style.overflow = 'hidden'; title.style.textOverflow = 'ellipsis'; title.style.flex = '1'; // 撑开剩余空间,把右侧按钮挤到最右边 const delBtn = document.createElement('button'); delBtn.innerHTML = t('delModel'); delBtn.style.padding = '6px 12px'; delBtn.style.background = '#ff4444'; delBtn.style.color = '#fff'; delBtn.style.border = 'none'; delBtn.style.borderRadius = '4px'; delBtn.style.cursor = 'pointer'; delBtn.style.marginLeft = 'auto'; // push to the right delBtn.style.whiteSpace = 'nowrap'; delBtn.onclick = async () => { if (!confirm(`${t('delConfirm')} ${model.filename} ${t('delConfirm2')}`)) return; try { const res = await fetch('/anomalous/delete_model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder, filename: model.filename }) }); const data = await res.json(); if (data.status === 'success') { if (this.currentDetailObserver) { this.currentDetailObserver.disconnect(); this.currentDetailObserver = null; } alert(t('delSuccess') + data.deleted.join('\n') + t('delNote')); this.detailPanel.style.display = 'none'; this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; this.grid.style.display = 'grid'; this.loadModels(); // refresh grid } else { alert(t('delFail') + data.message); } } catch (e) { alert(t('delFail') + e.message); } }; const jumpBtn = document.createElement('button'); jumpBtn.innerHTML = '⬇️'; jumpBtn.title = t('jumpToBottom') || 'Jump to bottom'; jumpBtn.style.padding = '6px 12px'; jumpBtn.style.background = '#444'; jumpBtn.style.color = '#fff'; jumpBtn.style.border = 'none'; jumpBtn.style.borderRadius = '4px'; jumpBtn.style.cursor = 'pointer'; jumpBtn.style.marginLeft = '10px'; jumpBtn.onclick = () => { // Find rightPanel which is created later, so we bind it dynamically const rp = this.detailPanel.querySelector('.anomalous-split-right'); if (rp) rp.scrollTo({ top: rp.scrollHeight, behavior: 'smooth' }); }; header.appendChild(backBtn); header.appendChild(title); header.appendChild(delBtn); header.appendChild(jumpBtn); const applyDetailBtn = document.createElement('button'); applyDetailBtn.innerHTML = t('applyToCanvas') || 'Apply to Canvas'; applyDetailBtn.style.padding = '6px 12px'; applyDetailBtn.style.background = '#007bff'; applyDetailBtn.style.color = '#fff'; applyDetailBtn.style.border = 'none'; applyDetailBtn.style.borderRadius = '4px'; applyDetailBtn.style.cursor = 'pointer'; applyDetailBtn.style.marginLeft = '10px'; applyDetailBtn.style.fontWeight = 'bold'; applyDetailBtn.onclick = () => { this.applyModelToCanvas(this.currentType, this.currentSubfolder, model); }; header.appendChild(applyDetailBtn); const splitContainer = document.createElement('div'); splitContainer.className = 'anomalous-split-container'; const leftPanel = document.createElement('div'); leftPanel.className = 'anomalous-split-left'; let isMediaRendered = null; const renderMedia = (shouldRender) => { if (shouldRender === isMediaRendered) return; isMediaRendered = shouldRender; leftPanel.innerHTML = ''; if (!shouldRender) return; if (model.preview_url) { const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i); if (isVideo) { const video = document.createElement('video'); video.src = model.preview_url; video.controls = true; video.autoplay = true; video.loop = true; video.style.width = '100%'; video.style.height = '100%'; video.style.objectFit = 'contain'; leftPanel.appendChild(video); } else { const img = document.createElement('img'); img.src = model.preview_url; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; leftPanel.appendChild(img); } } else { leftPanel.innerHTML = `
${t('noPreview')}
`; } }; const containerEl = document.getElementById('anomalous-container'); this.currentDetailObserver = new ResizeObserver(entries => { for (let entry of entries) { renderMedia(entry.contentRect.width >= 750); } }); this.currentDetailObserver.observe(containerEl); const resizer = document.createElement('div'); resizer.className = 'anomalous-resizer'; let isResizing = false; resizer.addEventListener('mousedown', (e) => { isResizing = true; document.body.style.cursor = 'col-resize'; e.preventDefault(); }); this.detailMouseMoveHandler = (e) => { if (!isResizing) return; const containerRect = splitContainer.getBoundingClientRect(); let newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100; if (newWidth < 20) newWidth = 20; if (newWidth > 80) newWidth = 80; leftPanel.style.width = newWidth + '%'; rightPanel.style.width = (100 - newWidth) + '%'; }; window.addEventListener('mousemove', this.detailMouseMoveHandler); this.detailMouseUpHandler = () => { if (isResizing) { isResizing = false; document.body.style.cursor = ''; } }; window.addEventListener('mouseup', this.detailMouseUpHandler); const rightPanel = document.createElement('div'); rightPanel.className = 'anomalous-split-right'; rightPanel.style.display = 'flex'; rightPanel.style.flexDirection = 'column'; rightPanel.style.height = '100%'; rightPanel.style.boxSizing = 'border-box'; rightPanel.style.overflow = 'auto'; rightPanel.style.padding = '15px'; rightPanel.style.color = '#eee'; const m = model.metadata || {}; // 1. Top Bar (Title + Size + Model + Button) in a single compact row if possible const topRow = document.createElement('div'); topRow.style.flexShrink = '0'; topRow.style.display = 'flex'; topRow.style.flexWrap = 'wrap'; topRow.style.alignItems = 'center'; topRow.style.gap = '10px'; topRow.style.paddingBottom = '10px'; topRow.style.marginBottom = '10px'; topRow.style.borderBottom = '1px solid #444'; const titleEl = document.createElement('h3'); titleEl.style.margin = '0'; titleEl.style.fontSize = '1.3em'; titleEl.style.marginRight = '10px'; titleEl.innerText = m.custom_name || m.name || model.filename; if (m.custom_name) { titleEl.style.color = '#88ff88'; } topRow.appendChild(titleEl); const metaSpan = document.createElement('span'); metaSpan.style.fontSize = '0.9em'; metaSpan.style.color = '#aaa'; metaSpan.innerHTML = `Size: ${escapeHtml(model.size_mb)} MB` + (m.baseModel ? ` Base: ${escapeHtml(m.baseModel)}` : ''); topRow.appendChild(metaSpan); if (m.civitai_url) { const cBtn = document.createElement('a'); cBtn.href = m.civitai_url; cBtn.target = '_blank'; cBtn.rel = 'noopener noreferrer'; cBtn.innerHTML = '🌐 Civitai'; cBtn.style.marginLeft = 'auto'; cBtn.style.padding = '4px 8px'; cBtn.style.background = '#1a73e8'; cBtn.style.color = '#fff'; cBtn.style.textDecoration = 'none'; cBtn.style.borderRadius = '4px'; cBtn.style.fontSize = '0.85em'; cBtn.style.fontWeight = 'bold'; topRow.appendChild(cBtn); } const editMetaBtn = document.createElement('button'); editMetaBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⚙️ 编辑' : '⚙️ Edit'; editMetaBtn.style.marginLeft = m.civitai_url ? '10px' : 'auto'; editMetaBtn.style.padding = '4px 8px'; editMetaBtn.style.background = '#444'; editMetaBtn.style.color = '#fff'; editMetaBtn.style.border = 'none'; editMetaBtn.style.borderRadius = '4px'; editMetaBtn.style.fontSize = '0.85em'; editMetaBtn.style.fontWeight = 'bold'; editMetaBtn.style.cursor = 'pointer'; editMetaBtn.onclick = () => { this.showEditModal(model); }; topRow.appendChild(editMetaBtn); rightPanel.appendChild(topRow); // 1.5 Custom Notes Section (Google Material Card) if (m.custom_notes) { const notesCard = document.createElement('div'); notesCard.style.flexShrink = '0'; notesCard.style.marginBottom = '15px'; notesCard.style.padding = '12px 16px'; // Dark yellowish/khaki paper background for dark mode notebook feel notesCard.style.background = 'linear-gradient(135deg, #262522 0%, #202124 100%)'; notesCard.style.border = '1px solid #3c4043'; notesCard.style.borderLeft = '4px solid #a38d53'; notesCard.style.borderRadius = '4px 8px 8px 4px'; notesCard.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'; notesCard.style.position = 'relative'; notesCard.style.fontFamily = 'Inter, Roboto, sans-serif'; // Faint notebook lines background notesCard.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)'; notesCard.style.backgroundAttachment = 'local'; // ensures lines scroll with text const notesHeader = document.createElement('div'); notesHeader.style.display = 'flex'; notesHeader.style.justifyContent = 'space-between'; notesHeader.style.alignItems = 'center'; notesHeader.style.marginBottom = '8px'; const notesTitle = document.createElement('div'); notesTitle.innerHTML = window.anomalous_browser_lang === 'zh' ? '📓 专属备注 (Notes)' : '📓 Custom Notes'; notesTitle.style.color = '#a38d53'; notesTitle.style.fontWeight = '600'; notesTitle.style.fontSize = '0.85em'; notesTitle.style.letterSpacing = '0.5px'; const notesEditBtn = document.createElement('button'); notesEditBtn.innerHTML = '✏️'; notesEditBtn.title = window.anomalous_browser_lang === 'zh' ? '编辑备注' : 'Edit Notes'; notesEditBtn.style.background = 'transparent'; notesEditBtn.style.border = 'none'; notesEditBtn.style.color = '#a38d53'; notesEditBtn.style.cursor = 'pointer'; notesEditBtn.style.padding = '2px'; notesEditBtn.style.fontSize = '1em'; notesEditBtn.style.borderRadius = '50%'; notesEditBtn.style.display = 'flex'; notesEditBtn.style.alignItems = 'center'; notesEditBtn.style.justifyContent = 'center'; notesEditBtn.style.opacity = '0.7'; notesEditBtn.onmouseover = () => notesEditBtn.style.opacity = '1'; notesEditBtn.onmouseout = () => notesEditBtn.style.opacity = '0.7'; notesEditBtn.onclick = () => { this.showEditModal(model); }; notesHeader.appendChild(notesTitle); notesHeader.appendChild(notesEditBtn); const notesContent = document.createElement('div'); notesContent.innerText = m.custom_notes; notesContent.style.color = '#d1c9b4'; // Warm off-white notesContent.style.fontSize = '0.95em'; notesContent.style.lineHeight = '24px'; // Matches the repeating gradient exactly notesContent.style.whiteSpace = 'pre-wrap'; notesContent.style.fontFamily = '"Consolas", "Courier New", monospace'; // Handwriting / typewriter feel // Removed text shadow for cleaner look notesCard.appendChild(notesHeader); notesCard.appendChild(notesContent); rightPanel.appendChild(notesCard); } // 1.8 Generated Gallery Button const galleryBtnCont = document.createElement('div'); galleryBtnCont.style.flexShrink = '0'; galleryBtnCont.style.marginBottom = '15px'; const galleryBtn = document.createElement('button'); galleryBtn.className = 'anomalous-nb-add-btn'; galleryBtn.style.width = '100%'; galleryBtn.style.display = 'flex'; galleryBtn.style.justifyContent = 'center'; galleryBtn.style.alignItems = 'center'; galleryBtn.style.gap = '8px'; galleryBtn.style.padding = '10px'; galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.border = '1px solid #3c4043'; galleryBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🖼️ 查看历史生成作品' : '🖼️ View Generated Gallery'; galleryBtn.onmouseover = () => { galleryBtn.style.background = '#3c4043'; galleryBtn.style.borderColor = '#8ab4f8'; }; galleryBtn.onmouseout = () => { galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.borderColor = '#3c4043'; }; galleryBtn.onclick = () => { this.showGeneratedGallery(model); }; galleryBtnCont.appendChild(galleryBtn); rightPanel.appendChild(galleryBtnCont); // 2. Trained Words Section if (m.trainedWords && m.trainedWords.length > 0) { const twCont = document.createElement('div'); twCont.style.flexShrink = '0'; twCont.style.marginBottom = '10px'; const twHeader = document.createElement('div'); twHeader.style.display = 'flex'; twHeader.style.alignItems = 'center'; twHeader.style.marginBottom = '5px'; const twLabel = document.createElement('strong'); twLabel.innerText = 'Trained Words:'; twHeader.appendChild(twLabel); const copyAll = document.createElement('button'); copyAll.innerText = t('copyAll'); copyAll.style.marginLeft = '10px'; copyAll.style.padding = '2px 6px'; copyAll.style.background = '#444'; copyAll.style.color = '#fff'; copyAll.style.border = 'none'; copyAll.style.borderRadius = '3px'; copyAll.style.cursor = 'pointer'; copyAll.style.fontSize = '0.8em'; copyAll.onclick = () => { const allWords = m.trainedWords.join(', '); navigator.clipboard.writeText(allWords).then(() => { const old = copyAll.innerText; copyAll.innerText = t('copied'); setTimeout(() => { copyAll.innerText = old; }, 1500); }); }; twHeader.appendChild(copyAll); twCont.appendChild(twHeader); const tagsCont = document.createElement('div'); tagsCont.style.display = 'flex'; tagsCont.style.flexWrap = 'wrap'; tagsCont.style.gap = '4px'; m.trainedWords.forEach(w => { const tag = document.createElement('span'); tag.innerText = w; tag.style.background = '#333'; tag.style.padding = '2px 6px'; tag.style.borderRadius = '4px'; tag.style.fontSize = '0.85em'; tag.style.cursor = 'pointer'; tag.style.border = '1px solid #555'; tag.title = t('clickToCopy') + w; tag.onclick = () => { navigator.clipboard.writeText(w).then(() => { tag.style.background = '#28a745'; setTimeout(() => { tag.style.background = '#333'; }, 500); }); }; tagsCont.appendChild(tag); }); twCont.appendChild(tagsCont); rightPanel.appendChild(twCont); } // 3. Description Section (Expands to fill remaining height) if (m.description) { const descCont = document.createElement('div'); descCont.style.flex = 'none'; descCont.style.display = 'flex'; descCont.style.flexDirection = 'column'; // important for flex scroll const descLabel = document.createElement('strong'); descLabel.innerText = 'Description:'; descLabel.style.marginBottom = '5px'; descCont.appendChild(descLabel); const descText = document.createElement('div'); descText.style.flex = 'none'; descText.style.background = '#222'; descText.style.padding = '10px'; descText.style.borderRadius = '6px'; descText.style.border = '1px solid #333'; descText.style.fontSize = '0.95em'; descText.style.lineHeight = '1.4'; setSafeRichHtml(descText, m.description); descCont.appendChild(descText); rightPanel.appendChild(descCont); } // 4. Notes Section if (m.notes) { const notesCont = document.createElement('div'); notesCont.style.flexShrink = '0'; notesCont.style.marginTop = '10px'; const notesLabel = document.createElement('strong'); notesLabel.innerText = 'Notes:'; notesCont.appendChild(notesLabel); const notesText = document.createElement('div'); notesText.style.background = '#332b00'; notesText.style.padding = '8px'; notesText.style.borderRadius = '6px'; notesText.style.marginTop = '5px'; notesText.style.border = '1px solid #554400'; notesText.style.fontSize = '0.9em'; setSafeRichHtml(notesText, m.notes); notesCont.appendChild(notesText); rightPanel.appendChild(notesCont); } // --- Compatible Models Section --- if (m.baseModel) { const compSec = document.createElement('div'); compSec.className = 'anomalous-compatible-section'; const compTitle = document.createElement('div'); compTitle.className = 'anomalous-compatible-title'; compTitle.innerHTML = `${t('compatibleModels') || '🔗 Compatible'} (${escapeHtml(m.baseModel)})`; const compList = document.createElement('div'); compList.className = 'anomalous-compatible-list'; compList.innerHTML = `${t('loadingCompatible') || 'Loading...'}`; compSec.appendChild(compTitle); compSec.appendChild(compList); rightPanel.appendChild(compSec); const targetType = this.currentType === 'loras' ? 'checkpoints,unet,diffusion_models' : 'loras'; fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(m.baseModel)}&target_type=${encodeURIComponent(targetType)}`) .then(r => r.json()) .then(d => { compList.innerHTML = ''; if (window.anomalous_update_hash_cache && d.models) { window.anomalous_update_hash_cache(d.models); } if (!d.models || d.models.length === 0) { compList.innerHTML = `No compatible models found.`; return; } d.models.forEach(m_comp => { const mItem = document.createElement('div'); mItem.className = 'anomalous-compatible-item'; mItem.title = m_comp.filename; let thumb = ''; if (m_comp.preview_url) { const isVid = m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i); if (isVid) thumb = ``; else thumb = ``; } else { thumb = `
?
`; } mItem.innerHTML = `${thumb}
${escapeHtml(m_comp.filename)}
`; if (m_comp.preview_url && (m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i))) { mItem.onmouseenter = () => { const v = mItem.querySelector('video'); if (v) v.play().catch(e => { }); }; mItem.onmouseleave = () => { const v = mItem.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } }; } mItem.onclick = () => { this.historyStack.push({ type: this.currentType, pathIdx: this.currentPathIdx, subfolder: this.currentSubfolder, model: this.currentDetailModel }); this.currentType = m_comp.type; this.currentPathIdx = m_comp.path_idx; this.currentSubfolder = m_comp.subfolder; this.currentDetailModel = m_comp; this.renderSidebar(); this.showDetail(m_comp); }; compList.appendChild(mItem); }); }) .catch(e => { compList.innerHTML = `Failed to load.`; }); } // --------------------------------- splitContainer.appendChild(leftPanel); splitContainer.appendChild(resizer); splitContainer.appendChild(rightPanel); this.detailPanel.appendChild(header); this.detailPanel.appendChild(splitContainer); } export function showEditModal(model) { const modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '0'; modal.style.left = '0'; modal.style.width = '100vw'; modal.style.height = '100vh'; modal.style.background = 'rgba(0,0,0,0.6)'; modal.style.backdropFilter = 'blur(4px)'; modal.style.zIndex = '10000'; modal.style.display = 'flex'; modal.style.alignItems = 'center'; modal.style.justifyContent = 'center'; const content = document.createElement('div'); content.style.background = '#202124'; content.style.padding = '24px'; content.style.borderRadius = '12px'; content.style.width = '720px'; content.style.maxWidth = '90%'; content.style.border = '1px solid #3c4043'; content.style.boxShadow = '0 8px 32px rgba(0,0,0,0.5)'; content.style.display = 'flex'; content.style.flexDirection = 'row'; content.style.gap = '24px'; content.style.fontFamily = 'Inter, Roboto, sans-serif'; // --- LEFT COLUMN --- const leftCol = document.createElement('div'); leftCol.style.width = '240px'; leftCol.style.flexShrink = '0'; leftCol.style.display = 'flex'; leftCol.style.flexDirection = 'column'; leftCol.style.gap = '12px'; const previewContainer = document.createElement('div'); previewContainer.style.width = '100%'; previewContainer.style.height = '320px'; previewContainer.style.background = '#303134'; previewContainer.style.borderRadius = '8px'; previewContainer.style.display = 'flex'; previewContainer.style.alignItems = 'center'; previewContainer.style.justifyContent = 'center'; previewContainer.style.overflow = 'hidden'; previewContainer.style.border = '1px solid #3c4043'; previewContainer.style.position = 'relative'; if (model.preview_url) { const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i); if (isVideo) { const video = document.createElement('video'); video.src = model.preview_url; video.controls = false; video.autoplay = true; video.loop = true; video.muted = true; video.style.width = '100%'; video.style.height = '100%'; video.style.objectFit = 'cover'; const muteBtn = document.createElement('div'); muteBtn.innerHTML = '🔇'; muteBtn.style.position = 'absolute'; muteBtn.style.bottom = '8px'; muteBtn.style.right = '8px'; muteBtn.style.background = 'rgba(0,0,0,0.6)'; muteBtn.style.color = '#fff'; muteBtn.style.padding = '6px'; muteBtn.style.borderRadius = '50%'; muteBtn.style.cursor = 'pointer'; muteBtn.style.fontSize = '14px'; muteBtn.style.zIndex = '10'; muteBtn.title = window.anomalous_browser_lang === 'zh' ? '开启/关闭声音' : 'Toggle Sound'; muteBtn.onclick = (e) => { e.stopPropagation(); video.muted = !video.muted; muteBtn.innerHTML = video.muted ? '🔇' : '🔊'; }; previewContainer.appendChild(video); previewContainer.appendChild(muteBtn); } else { const img = document.createElement('img'); img.src = model.preview_url; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'cover'; previewContainer.appendChild(img); } } else { previewContainer.innerHTML = `
${window.anomalous_browser_lang === 'zh' ? '暂无封面' : 'No Cover'}
`; } const coverRow = document.createElement('div'); coverRow.style.display = 'flex'; coverRow.style.flexDirection = 'column'; coverRow.style.gap = '8px'; const galleryBtn = document.createElement('button'); galleryBtn.innerHTML = '🖼️ ' + (window.anomalous_browser_lang === 'zh' ? '从历史图库挑选' : 'Pick from Gallery'); galleryBtn.style.padding = '8px'; galleryBtn.style.background = '#303134'; galleryBtn.style.color = '#8ab4f8'; galleryBtn.style.border = '1px solid #5f6368'; galleryBtn.style.borderRadius = '6px'; galleryBtn.style.cursor = 'pointer'; galleryBtn.style.fontWeight = '500'; galleryBtn.style.fontSize = '0.9em'; galleryBtn.onmouseover = () => galleryBtn.style.background = '#3c4043'; galleryBtn.onmouseout = () => galleryBtn.style.background = '#303134'; galleryBtn.onclick = () => { document.body.removeChild(modal); this.showGallerySelectMode(model); }; const localBtn = document.createElement('button'); localBtn.innerHTML = '📁 ' + (window.anomalous_browser_lang === 'zh' ? '从本地上传' : 'Upload Local'); localBtn.style.padding = '8px'; localBtn.style.background = '#303134'; localBtn.style.color = '#8ab4f8'; localBtn.style.border = '1px solid #5f6368'; localBtn.style.borderRadius = '6px'; localBtn.style.cursor = 'pointer'; localBtn.style.fontWeight = '500'; localBtn.style.fontSize = '0.9em'; localBtn.onmouseover = () => localBtn.style.background = '#3c4043'; localBtn.onmouseout = () => localBtn.style.background = '#303134'; localBtn.onclick = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.onchange = async (e) => { if (e.target.files && e.target.files.length > 0) { const file = e.target.files[0]; const formData = new FormData(); formData.append('type', this.currentType); formData.append('path_idx', this.currentPathIdx); formData.append('subfolder', this.currentSubfolder); formData.append('filename', model.filename); formData.append('image', file); try { const res = await fetch('/anomalous/upload_custom_cover', { method: 'POST', body: formData }); const data = await res.json(); if (data.status === 'success') { await this.loadModels(); const updatedModel = this.models.find(m => m.filename === model.filename); if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) { if (updatedModel) this.showDetail(updatedModel); } document.body.removeChild(modal); } else { alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); } } catch (err) { alert((window.anomalous_browser_lang === 'zh' ? '上传失败: ' : 'Upload failed: ') + err); } } }; fileInput.click(); }; coverRow.appendChild(galleryBtn); coverRow.appendChild(localBtn); leftCol.appendChild(previewContainer); leftCol.appendChild(coverRow); // --- RIGHT COLUMN --- const rightCol = document.createElement('div'); rightCol.style.flex = '1'; rightCol.style.display = 'flex'; rightCol.style.flexDirection = 'column'; rightCol.style.gap = '15px'; const title = document.createElement('h2'); title.innerText = window.anomalous_browser_lang === 'zh' ? '编辑模型信息' : 'Edit Model Info'; title.style.margin = '0'; title.style.color = '#e8eaed'; title.style.fontSize = '1.25em'; title.style.fontWeight = '500'; const filenameLabel = document.createElement('div'); filenameLabel.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '文件:' : 'File:'} ${escapeHtml(model.filename)}`; filenameLabel.style.color = '#e8eaed'; filenameLabel.style.fontSize = '0.9em'; filenameLabel.style.wordBreak = 'break-all'; const inputStyle = ` width: 100%; padding: 12px 14px; background: #303134; color: #e8eaed; border: 1px solid #5f6368; border-radius: 6px; box-sizing: border-box; outline: none; font-size: 14px; transition: border 0.2s; `; const nameInput = document.createElement('input'); nameInput.placeholder = window.anomalous_browser_lang === 'zh' ? '自定义名称 (留空则使用原名)' : 'Custom Name (Leave empty to use original)'; nameInput.value = (model.metadata && model.metadata.custom_name) ? model.metadata.custom_name : ''; nameInput.style.cssText = inputStyle; nameInput.onfocus = () => nameInput.style.borderColor = '#8ab4f8'; nameInput.onblur = () => nameInput.style.borderColor = '#5f6368'; const notesInput = document.createElement('textarea'); notesInput.placeholder = window.anomalous_browser_lang === 'zh' ? '📓 专属备注... (支持多行)' : '📓 Custom Notes...'; notesInput.value = (model.metadata && model.metadata.custom_notes) ? model.metadata.custom_notes : ''; notesInput.style.cssText = inputStyle; notesInput.style.flex = '1'; // fill remaining space notesInput.style.minHeight = '150px'; notesInput.style.resize = 'vertical'; // Notebook styling override notesInput.style.background = 'linear-gradient(135deg, #262522 0%, #202124 100%)'; notesInput.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)'; notesInput.style.backgroundAttachment = 'local'; notesInput.style.border = '1px solid #3c4043'; notesInput.style.borderLeft = '4px solid #a38d53'; notesInput.style.borderRadius = '4px 8px 8px 4px'; notesInput.style.color = '#d1c9b4'; notesInput.style.fontFamily = '"Consolas", "Courier New", monospace'; notesInput.style.lineHeight = '24px'; // Removed text shadow for cleaner look notesInput.onfocus = () => { notesInput.style.boxShadow = '0 0 0 2px rgba(163, 141, 83, 0.2)'; notesInput.style.borderColor = '#a38d53'; }; notesInput.onblur = () => { notesInput.style.boxShadow = 'none'; notesInput.style.borderColor = '#3c4043'; }; const physicalRow = document.createElement('div'); physicalRow.style.display = 'flex'; physicalRow.style.flexDirection = 'column'; physicalRow.style.gap = '4px'; const physicalCheckboxWrapper = document.createElement('div'); physicalCheckboxWrapper.style.display = 'flex'; physicalCheckboxWrapper.style.alignItems = 'center'; physicalCheckboxWrapper.style.gap = '8px'; const physicalCheckbox = document.createElement('input'); physicalCheckbox.type = 'checkbox'; physicalCheckbox.id = 'anomalous-physical-rename-checkbox'; physicalCheckbox.style.cursor = 'pointer'; const physicalLabel = document.createElement('label'); physicalLabel.htmlFor = 'anomalous-physical-rename-checkbox'; physicalLabel.innerText = window.anomalous_browser_lang === 'zh' ? '同步物理重命名' : 'Physical Rename on Disk'; physicalLabel.style.color = '#e8eaed'; physicalLabel.style.fontSize = '0.9em'; physicalLabel.style.cursor = 'pointer'; physicalCheckboxWrapper.appendChild(physicalCheckbox); physicalCheckboxWrapper.appendChild(physicalLabel); const physicalDesc = document.createElement('div'); physicalDesc.style.fontSize = '0.8em'; physicalDesc.style.color = '#9aa0a6'; physicalDesc.style.marginLeft = '22px'; physicalDesc.innerText = window.anomalous_browser_lang === 'zh' ? '若不勾选则仅虚拟重命名,安全不破坏原文件。' : 'Virtual rename only if unchecked. Safe for originals.'; physicalRow.appendChild(physicalCheckboxWrapper); physicalRow.appendChild(physicalDesc); const actionRow = document.createElement('div'); actionRow.style.display = 'flex'; actionRow.style.justifyContent = 'space-between'; actionRow.style.marginTop = 'auto'; const leftActions = document.createElement('div'); leftActions.style.display = 'flex'; leftActions.style.gap = '10px'; const resetBtn = document.createElement('button'); resetBtn.innerText = window.anomalous_browser_lang === 'zh' ? '重置设置' : 'Reset All'; resetBtn.style.padding = '8px 16px'; resetBtn.style.background = 'transparent'; resetBtn.style.color = '#f28b82'; resetBtn.style.border = '1px solid #f28b82'; resetBtn.style.borderRadius = '4px'; resetBtn.style.cursor = 'pointer'; resetBtn.style.fontWeight = '500'; resetBtn.onclick = async () => { if (!confirm(window.anomalous_browser_lang === 'zh' ? '确定要重置该模型的所有自定义信息(名称、封面、备注)吗?' : 'Reset all custom info for this model?')) return; document.body.removeChild(modal); try { const res = await fetch('/anomalous/update_metadata', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder, filename: model.filename, custom_name: '', custom_notes: '', reset_cover: true, physical_rename: false }) }); const result = await res.json(); if (res.ok && result.status === 'success') { await this.loadModels(); const updatedModel = this.models.find(m => m.filename === model.filename); if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) { if (updatedModel) { this.showDetail(updatedModel); } else { this.grid.style.display = 'grid'; this.detailPanel.style.display = 'none'; } } if (result.cover_reset === false) { const noSource = result.cover_reset_source === 'preserved_current'; alert(window.anomalous_browser_lang === 'zh' ? (noSource ? '未找到可恢复的 C 站备份或原始封面。为避免丢失唯一图片,当前封面已保留;自定义名称和备注仍已重置。' : '封面恢复未能完整完成,请检查文件权限或占用情况;自定义名称和备注仍已重置。') : (noSource ? 'No recoverable Civitai backup or original cover was found. The current cover was kept to avoid losing the only image; the custom name and notes were still reset.' : 'The cover restore could not be completed. Check file permissions or locks; the custom name and notes were still reset.')); } } else { alert(result.message || (window.anomalous_browser_lang === 'zh' ? '重置失败。' : 'Reset failed.')); } } catch (e) { console.error(e); } }; const cancelBtn = document.createElement('button'); cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel'; cancelBtn.style.padding = '8px 16px'; cancelBtn.style.background = 'transparent'; cancelBtn.style.color = '#8ab4f8'; cancelBtn.style.border = 'none'; cancelBtn.style.borderRadius = '4px'; cancelBtn.style.cursor = 'pointer'; cancelBtn.style.fontWeight = '500'; cancelBtn.onclick = () => document.body.removeChild(modal); leftActions.appendChild(resetBtn); leftActions.appendChild(cancelBtn); const saveBtn = document.createElement('button'); saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存更改' : 'Save Changes'; saveBtn.style.padding = '8px 24px'; saveBtn.style.background = '#8ab4f8'; saveBtn.style.color = '#202124'; saveBtn.style.border = 'none'; saveBtn.style.borderRadius = '4px'; saveBtn.style.cursor = 'pointer'; saveBtn.style.fontWeight = '600'; saveBtn.onclick = () => { const newName = nameInput.value.trim(); const newNotes = notesInput.value.trim(); saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存中...' : 'Saving...'; saveBtn.disabled = true; fetch('/anomalous/update_metadata', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder, filename: model.filename, custom_name: newName, custom_notes: newNotes, physical_rename: physicalCheckbox.checked }) }).then(res => res.json()).then(data => { document.body.removeChild(modal); if (data.status === 'success') { if (!model.metadata) model.metadata = {}; model.metadata.custom_name = newName; model.metadata.custom_notes = newNotes; if (data.new_filename && physicalCheckbox.checked) { model.filename = data.new_filename; } this.loadModels(); if (this.currentDetailModel && (this.currentDetailModel.filename === model.filename || (data.new_filename && this.currentDetailModel.filename === data.new_filename))) { this.showDetail(model); } } else { alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); } }).catch(e => { document.body.removeChild(modal); alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + e); }); }; actionRow.appendChild(leftActions); actionRow.appendChild(saveBtn); rightCol.appendChild(title); rightCol.appendChild(filenameLabel); rightCol.appendChild(nameInput); rightCol.appendChild(notesInput); rightCol.appendChild(physicalRow); rightCol.appendChild(actionRow); content.appendChild(leftCol); content.appendChild(rightCol); modal.appendChild(content); document.body.appendChild(modal); } export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) { let localSelection = new Map(); for (const [k, v] of initialSelectedMap.entries()) { localSelection.set(k, new Set(v)); } const getFolderSelectionCount = (folderKey) => { let nKey = folderKey; const parts = folderKey.split('|'); if (parts.length >= 3 && parts[2].startsWith('/')) { parts[2] = parts[2].substring(1); nKey = parts.join('|'); } return localSelection.has(nKey) ? localSelection.get(nKey).size : 0; }; const modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '0'; modal.style.left = '0'; modal.style.width = '100vw'; modal.style.height = '100vh'; modal.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; modal.style.zIndex = '9999999'; modal.style.display = 'flex'; modal.style.justifyContent = 'center'; modal.style.alignItems = 'center'; modal.style.fontFamily = 'Roboto, Inter, sans-serif'; const contentDiv = document.createElement('div'); contentDiv.style.background = '#202124'; contentDiv.style.borderRadius = '8px'; contentDiv.style.width = '92vw'; contentDiv.style.height = '88vh'; contentDiv.style.display = 'flex'; contentDiv.style.flexDirection = 'column'; contentDiv.style.boxShadow = '0 24px 38px 3px rgba(0,0,0,0.4), 0 9px 46px 8px rgba(0,0,0,0.3), 0 11px 15px -7px rgba(0,0,0,0.5)'; contentDiv.style.overflow = 'hidden'; // Header const header = document.createElement('div'); header.style.padding = '16px 24px'; header.style.borderBottom = '1px solid #3c4043'; header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; const titleContainer = document.createElement('div'); titleContainer.style.display = 'flex'; titleContainer.style.alignItems = 'center'; titleContainer.style.gap = '12px'; const title = document.createElement('h2'); title.innerText = window.anomalous_browser_lang === 'zh' ? '选择模型' : 'Select Models'; title.style.margin = '0'; title.style.color = '#e8eaed'; title.style.fontSize = '20px'; title.style.fontWeight = '500'; titleContainer.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.innerHTML = '✕'; closeBtn.style.background = 'transparent'; closeBtn.style.color = '#9aa0a6'; closeBtn.style.border = 'none'; closeBtn.style.fontSize = '18px'; closeBtn.style.cursor = 'pointer'; closeBtn.onclick = () => document.body.removeChild(modal); header.appendChild(titleContainer); header.appendChild(closeBtn); contentDiv.appendChild(header); // Body area const body = document.createElement('div'); body.style.display = 'flex'; body.style.flex = '1'; body.style.overflow = 'hidden'; // Left Panel const leftPanel = document.createElement('div'); leftPanel.style.width = '280px'; leftPanel.style.borderRight = '1px solid #3c4043'; leftPanel.style.display = 'flex'; leftPanel.style.flexDirection = 'column'; leftPanel.style.background = '#171717'; const leftTitle = document.createElement('div'); leftTitle.innerText = window.anomalous_browser_lang === 'zh' ? '目录列表' : 'Folders'; leftTitle.style.padding = '16px 24px'; leftTitle.style.fontWeight = '500'; leftTitle.style.color = '#9aa0a6'; leftTitle.style.fontSize = '14px'; leftTitle.style.letterSpacing = '0.5px'; leftPanel.appendChild(leftTitle); const treeContainer = document.createElement('div'); treeContainer.style.flex = '1'; treeContainer.style.overflowY = 'auto'; treeContainer.style.padding = '0 12px 12px 12px'; leftPanel.appendChild(treeContainer); // Right Panel const rightPanel = document.createElement('div'); rightPanel.style.flex = '1'; rightPanel.style.display = 'flex'; rightPanel.style.flexDirection = 'column'; rightPanel.style.background = '#202124'; // Toolbar const toolbar = document.createElement('div'); toolbar.style.padding = '12px 24px'; toolbar.style.borderBottom = '1px solid #3c4043'; toolbar.style.display = 'flex'; toolbar.style.gap = '12px'; toolbar.style.alignItems = 'center'; toolbar.style.flexWrap = 'wrap'; const createTBtn = (text, onClick, primary = false) => { const b = document.createElement('button'); b.innerText = text; b.style.padding = '6px 16px'; b.style.background = primary ? 'rgba(138,180,248,0.15)' : 'rgba(255,255,255,0.04)'; b.style.color = primary ? '#8ab4f8' : '#e8eaed'; b.style.border = primary ? '1px solid rgba(138,180,248,0.2)' : '1px solid #5f6368'; b.style.borderRadius = '4px'; b.style.cursor = 'pointer'; b.style.fontSize = '14px'; b.style.fontWeight = '500'; b.style.transition = 'background-color 0.15s, box-shadow 0.15s'; b.onmouseover = () => { b.style.background = primary ? 'rgba(138,180,248,0.25)' : 'rgba(255,255,255,0.1)'; }; b.onmouseout = () => { b.style.background = primary ? 'rgba(138,180,248,0.15)' : 'rgba(255,255,255,0.04)'; }; b.onclick = onClick; return b; }; let currentModels = []; let currentTotal = 0; let currentFolderKey = 'ALL'; let currentBaseUrl = ''; let currentPage = 1; let isLoading = false; const updateSummaryAndBadges = () => { let total = 0; for (const set of localSelection.values()) total += set.size; if (total > 0) { summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? `已选 ${total} 项` : `Selected ${total} items`; } else { summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? '暂未选择' : 'Nothing selected'; } treeContainer.querySelectorAll('.folder-item').forEach(fBtn => { const fKey = fBtn.dataset.folderKey; const count = getFolderSelectionCount(fKey); const badge = fBtn.querySelector('.selection-badge'); if (badge) { if (count > 0) { badge.innerText = count; badge.style.display = 'inline-flex'; } else { badge.style.display = 'none'; } } }); // Also update total models text in grid const statusDiv = grid.querySelector('.status-text'); if (statusDiv && !isLoading) { statusDiv.innerText = window.anomalous_browser_lang === 'zh' ? `已加载 ${currentModels.length} / ${currentTotal} 个模型` : `Loaded ${currentModels.length} / ${currentTotal} models`; } }; const updateGridCheckboxes = () => { grid.querySelectorAll('.model-card').forEach(card => { const filename = card.dataset.filename; const mType = card.dataset.mtype; const mPathIdx = card.dataset.mpathidx; const mSubfolder = card.dataset.msubfolder; const fKey = `${mType}|${mPathIdx}|${mSubfolder}`; const set = localSelection.get(fKey) || new Set(); const isChecked = set.has(filename); const cbWrapper = card.querySelector('.cb-wrapper'); const cb = card.querySelector('input[type="checkbox"]'); cb.checked = isChecked; if (isChecked) { card.style.border = '2px solid #8ab4f8'; card.style.background = 'rgba(138,180,248,0.2)'; cbWrapper.style.background = '#8ab4f8'; cbWrapper.style.border = '2px solid #8ab4f8'; cbWrapper.querySelector('span').style.color = '#202124'; } else { card.style.border = '1px solid #3c4043'; card.style.background = '#303134'; cbWrapper.style.background = 'rgba(0,0,0,0.3)'; cbWrapper.style.border = '2px solid #5f6368'; cbWrapper.querySelector('span').style.color = 'transparent'; } }); updateSummaryAndBadges(); }; const handleBatchSelect = async (action) => { if (action === 'none') { if (currentFolderKey === 'ALL') { localSelection.clear(); } else { let nCurKey = currentFolderKey; const cParts = currentFolderKey.split('|'); if (cParts.length >= 3 && cParts[2].startsWith('/')) { cParts[2] = cParts[2].substring(1); nCurKey = cParts.join('|'); } localSelection.delete(nCurKey); } updateGridCheckboxes(); return; } summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? '计算中...' : 'Computing...'; try { const res = await fetch(`/anomalous/batch_select?folderKey=${encodeURIComponent(currentFolderKey)}&action=${action}`); if (!res.ok) throw new Error('API failed'); const data = await res.json(); for (const [fKey, files] of Object.entries(data.selected)) { let normalizedKey = fKey; const parts = fKey.split('|'); if (parts.length >= 3 && parts[2].startsWith('/')) { parts[2] = parts[2].substring(1); normalizedKey = parts.join('|'); } const set = localSelection.get(normalizedKey) || new Set(); files.forEach(f => set.add(f)); localSelection.set(normalizedKey, set); } updateGridCheckboxes(); } catch (e) { console.error(e); summaryText.innerHTML = 'Error'; setTimeout(() => updateGridCheckboxes(), 2000); } }; toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '选无预览图' : 'Select No Preview', () => handleBatchSelect('no_preview'), true)); toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '选无介绍' : 'Select No Desc', () => handleBatchSelect('no_desc'), true)); const divi = document.createElement('div'); divi.style.width = '1px'; divi.style.height = '24px'; divi.style.background = '#3c4043'; divi.style.margin = '0 8px'; toolbar.appendChild(divi); toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '全选' : 'Select All', () => handleBatchSelect('all'))); toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '清空' : 'Clear', () => handleBatchSelect('none'))); const summaryText = document.createElement('div'); summaryText.style.marginLeft = 'auto'; summaryText.style.color = '#9aa0a6'; summaryText.style.fontWeight = '400'; toolbar.appendChild(summaryText); rightPanel.appendChild(toolbar); const grid = document.createElement('div'); grid.style.flex = '1'; grid.style.overflowY = 'auto'; grid.style.padding = '24px'; grid.style.display = 'grid'; grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(180px, 1fr))'; grid.style.gridAutoRows = 'max-content'; grid.style.gap = '20px'; grid.style.alignContent = 'start'; grid.style.background = '#171717'; rightPanel.appendChild(grid); body.appendChild(leftPanel); body.appendChild(rightPanel); contentDiv.appendChild(body); // Footer (Confirm) const footer = document.createElement('div'); footer.style.padding = '12px 24px'; footer.style.borderTop = '1px solid #3c4043'; footer.style.display = 'flex'; footer.style.justifyContent = 'flex-end'; footer.style.gap = '12px'; const cancelBtn = document.createElement('button'); cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel'; cancelBtn.style.padding = '8px 24px'; cancelBtn.style.background = 'transparent'; cancelBtn.style.color = '#8ab4f8'; cancelBtn.style.border = 'none'; cancelBtn.style.borderRadius = '4px'; cancelBtn.style.cursor = 'pointer'; cancelBtn.style.fontWeight = '500'; cancelBtn.style.fontSize = '14px'; cancelBtn.onmouseover = () => cancelBtn.style.background = 'rgba(138,180,248,0.1)'; cancelBtn.onmouseout = () => cancelBtn.style.background = 'transparent'; cancelBtn.onclick = () => document.body.removeChild(modal); const confirmBtn = document.createElement('button'); confirmBtn.innerText = window.anomalous_browser_lang === 'zh' ? '确定' : 'Confirm'; confirmBtn.style.padding = '8px 24px'; confirmBtn.style.background = '#8ab4f8'; confirmBtn.style.color = '#202124'; // dark text on bright accent button confirmBtn.style.border = 'none'; confirmBtn.style.borderRadius = '4px'; confirmBtn.style.cursor = 'pointer'; confirmBtn.style.fontWeight = '600'; confirmBtn.style.fontSize = '14px'; confirmBtn.style.boxShadow = '0 1px 2px 0 rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)'; confirmBtn.onmouseover = () => { confirmBtn.style.background = '#aecbfa'; confirmBtn.style.boxShadow = '0 1px 3px 0 rgba(0,0,0,.3), 0 4px 8px 3px rgba(0,0,0,.15)'; }; confirmBtn.onmouseout = () => { confirmBtn.style.background = '#8ab4f8'; confirmBtn.style.boxShadow = '0 1px 2px 0 rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)'; }; confirmBtn.onclick = () => { onConfirm(localSelection); document.body.removeChild(modal); }; footer.appendChild(cancelBtn); footer.appendChild(confirmBtn); contentDiv.appendChild(footer); modal.appendChild(contentDiv); document.body.appendChild(modal); // Infinite Scroll Logic const renderCards = (modelsToRender) => { const frag = document.createDocumentFragment(); modelsToRender.forEach(m => { const card = document.createElement('div'); card.className = 'model-card'; card.dataset.filename = m.filename; card.dataset.mtype = m.type; card.dataset.mpathidx = m.path_idx; card.dataset.msubfolder = m.subfolder; card.style.background = '#303134'; card.style.borderRadius = '8px'; card.style.overflow = 'hidden'; card.style.position = 'relative'; card.style.border = '1px solid #3c4043'; card.style.display = 'flex'; card.style.flexDirection = 'column'; card.style.aspectRatio = '2 / 3'; card.style.height = 'auto'; card.style.cursor = 'pointer'; card.style.boxShadow = '0 1px 2px 0 rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)'; const imgContainer = document.createElement('div'); imgContainer.style.flex = '1'; imgContainer.style.background = '#202124'; imgContainer.style.display = 'flex'; imgContainer.style.justifyContent = 'center'; imgContainer.style.alignItems = 'center'; imgContainer.style.overflow = 'hidden'; if (m.preview_url) { const isVid = m.preview_url.match(/\.(mp4|webm|mov|avi)(?:&|$)/i); if (isVid) { const video = document.createElement('video'); video.src = m.preview_url; video.style.width = '100%'; video.style.height = '100%'; video.style.objectFit = 'cover'; video.muted = true; video.loop = true; video.playsInline = true; card.addEventListener('mouseenter', () => video.play().catch(e => {})); card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; }); imgContainer.appendChild(video); } else { const img = document.createElement('img'); img.src = m.preview_url; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'cover'; imgContainer.appendChild(img); } } else { imgContainer.innerHTML = '📄'; } card.appendChild(imgContainer); const nameBar = document.createElement('div'); nameBar.style.padding = '12px'; nameBar.style.background = '#303134'; nameBar.style.fontSize = '13px'; nameBar.style.color = '#e8eaed'; nameBar.style.whiteSpace = 'nowrap'; nameBar.style.overflow = 'hidden'; nameBar.style.textOverflow = 'ellipsis'; nameBar.style.borderTop = '1px solid #3c4043'; nameBar.innerText = m.filename; card.appendChild(nameBar); const cbWrapper = document.createElement('div'); cbWrapper.className = 'cb-wrapper'; cbWrapper.style.position = 'absolute'; cbWrapper.style.top = '8px'; cbWrapper.style.right = '8px'; cbWrapper.style.width = '20px'; cbWrapper.style.height = '20px'; cbWrapper.style.borderRadius = '50%'; cbWrapper.style.background = 'rgba(0,0,0,0.3)'; cbWrapper.style.border = '2px solid #5f6368'; cbWrapper.style.display = 'flex'; cbWrapper.style.justifyContent = 'center'; cbWrapper.style.alignItems = 'center'; cbWrapper.style.pointerEvents = 'none'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.style.display = 'none'; const checkIcon = document.createElement('span'); checkIcon.innerHTML = '✓'; checkIcon.style.color = 'transparent'; checkIcon.style.fontSize = '12px'; checkIcon.style.fontWeight = 'bold'; cbWrapper.appendChild(checkIcon); cbWrapper.appendChild(cb); card.appendChild(cbWrapper); card.onclick = () => { const mKey = `${m.type}|${m.path_idx}|${m.subfolder}`; const cset = localSelection.get(mKey) || new Set(); if (!cset.has(m.filename)) { cset.add(m.filename); } else { cset.delete(m.filename); } localSelection.set(mKey, cset); updateGridCheckboxes(); }; frag.appendChild(card); }); return frag; }; const fetchModelsPage = async (isLoadMore = false) => { if (isLoading) return; isLoading = true; const prevStatus = grid.querySelector('.status-text'); if (prevStatus) grid.removeChild(prevStatus); const loadingIndicator = document.createElement('div'); loadingIndicator.className = 'status-text'; loadingIndicator.style.gridColumn = '1 / -1'; loadingIndicator.style.padding = '40px'; loadingIndicator.style.textAlign = 'center'; loadingIndicator.style.color = '#9aa0a6'; loadingIndicator.style.fontSize = '16px'; loadingIndicator.innerText = window.anomalous_browser_lang === 'zh' ? '加载中 (Loading)...' : 'Loading...'; grid.appendChild(loadingIndicator); try { const sep = currentBaseUrl.includes('?') ? '&' : '?'; const url = `${currentBaseUrl}${sep}page=${currentPage}&limit=50`; const res = await fetch(url); if (!res.ok) { grid.innerHTML = `
❌ 请求后端 API 失败!

你必须重启 ComfyUI 进程,刚写入后端的代码才能生效。
`; isLoading = false; return; } const data = await res.json(); const fetchedModels = data.models || []; currentTotal = data.total || 0; if (isLoadMore) { currentModels = currentModels.concat(fetchedModels); } else { currentModels = fetchedModels; // Keep the loading indicator temporarily, we clear grid except it Array.from(grid.children).forEach(c => { if (c !== loadingIndicator) grid.removeChild(c); }); } grid.removeChild(loadingIndicator); if (currentModels.length === 0 && !isLoadMore) { grid.innerHTML = `
No models found.
`; } else { grid.appendChild(renderCards(fetchedModels)); updateGridCheckboxes(); // Show status at bottom const statusText = document.createElement('div'); statusText.className = 'status-text'; statusText.style.gridColumn = '1 / -1'; statusText.style.padding = '20px'; statusText.style.textAlign = 'center'; statusText.style.color = '#5f6368'; statusText.style.fontSize = '14px'; if (currentModels.length >= currentTotal) { statusText.innerText = window.anomalous_browser_lang === 'zh' ? '✅ 已加载全部' : '✅ All models loaded'; } else { statusText.innerText = window.anomalous_browser_lang === 'zh' ? '滑动加载更多...' : 'Scroll for more...'; } grid.appendChild(statusText); } } catch (e) { console.error(e); grid.removeChild(loadingIndicator); } isLoading = false; }; grid.onscroll = () => { if (isLoading || currentModels.length >= currentTotal) return; // Near bottom detection if (grid.scrollTop + grid.clientHeight >= grid.scrollHeight - 150) { currentPage++; fetchModelsPage(true); } }; // Render Folders const renderFolders = async () => { treeContainer.innerHTML = ''; const allBtn = document.createElement('div'); allBtn.className = 'folder-item'; allBtn.dataset.folderKey = 'ALL'; allBtn.style.padding = '10px 16px'; allBtn.style.cursor = 'pointer'; allBtn.style.color = '#e8eaed'; allBtn.style.fontSize = '14px'; allBtn.style.fontWeight = '500'; allBtn.style.borderRadius = '0 16px 16px 0'; allBtn.style.marginBottom = '8px'; allBtn.style.display = 'flex'; allBtn.style.justifyContent = 'space-between'; allBtn.style.alignItems = 'center'; allBtn.innerHTML = `🌟 ${window.anomalous_browser_lang === 'zh' ? '全部模型' : 'All Models'}`; let loadFolder = async (fBtn, folderKey, fetchUrl) => { treeContainer.querySelectorAll('.folder-item').forEach(d => { d.style.background = 'transparent'; d.style.color = '#9aa0a6'; d.style.fontWeight = '400'; }); fBtn.style.background = 'rgba(138,180,248,0.15)'; fBtn.style.color = '#8ab4f8'; fBtn.style.fontWeight = '500'; currentFolderKey = folderKey; currentBaseUrl = fetchUrl; currentPage = 1; currentModels = []; currentTotal = 0; grid.innerHTML = ''; await fetchModelsPage(false); }; allBtn.onclick = () => loadFolder(allBtn, 'ALL', '/anomalous/all_scan_models'); treeContainer.appendChild(allBtn); if (!this.foldersData) { const res = await fetch('/anomalous/folders'); const data = await res.json(); this.foldersData = data.folders || []; } this.foldersData.forEach(tData => { const typeItem = document.createElement('div'); typeItem.style.marginBottom = '4px'; const tTitle = document.createElement('div'); tTitle.innerText = tData.label; tTitle.style.color = '#9aa0a6'; tTitle.style.fontWeight = '500'; tTitle.style.padding = '8px 16px'; tTitle.style.fontSize = '12px'; tTitle.style.marginTop = '8px'; typeItem.appendChild(tTitle); for (const [path, fData] of Object.entries(tData.folders)) { if (fData.model_count === 0 && !fData.has_models) continue; const folderKey = `${tData.type}|${tData.path_idx}|${path}`; const fBtn = document.createElement('div'); fBtn.className = 'folder-item'; fBtn.dataset.folderKey = folderKey; const depth = path.split('/').length - 1; fBtn.style.padding = `8px 16px 8px ${16 + depth * 16}px`; fBtn.style.cursor = 'pointer'; fBtn.style.color = '#9aa0a6'; fBtn.style.fontSize = '14px'; fBtn.style.borderRadius = '0 16px 16px 0'; fBtn.style.display = 'flex'; fBtn.style.justifyContent = 'space-between'; fBtn.style.alignItems = 'center'; const leftPart = document.createElement('div'); leftPart.style.display = 'flex'; leftPart.style.alignItems = 'center'; leftPart.style.gap = '8px'; leftPart.innerHTML = `📁 ${escapeHtml(fData.name)} (${escapeHtml(fData.model_count)})`; const badge = document.createElement('div'); badge.className = 'selection-badge'; badge.style.background = '#8ab4f8'; badge.style.color = '#202124'; badge.style.fontSize = '11px'; badge.style.fontWeight = '500'; badge.style.padding = '1px 6px'; badge.style.borderRadius = '10px'; badge.style.display = 'none'; fBtn.appendChild(leftPart); fBtn.appendChild(badge); fBtn.onmouseover = () => { if (currentFolderKey !== folderKey) fBtn.style.background = 'rgba(255,255,255,0.04)'; }; fBtn.onmouseout = () => { if (currentFolderKey !== folderKey) fBtn.style.background = 'transparent'; }; fBtn.onclick = () => { const params = new URLSearchParams({ type: tData.type, path_idx: tData.path_idx, subfolder: path }); loadFolder(fBtn, folderKey, '/anomalous/models?' + params.toString()); }; typeItem.appendChild(fBtn); } treeContainer.appendChild(typeItem); }); allBtn.onclick(); }; renderFolders(); } export function setWidgetValuePath(node, relPath) { if (!node.widgets || node.widgets.length === 0) return; const w = node.widgets.find(wg => wg.type === 'combo'); const targetWidget = w || node.widgets[0]; if (!targetWidget) return; if (targetWidget.options && targetWidget.options.values) { const normalizedTarget = relPath.replace(/\\/g, '/'); const match = targetWidget.options.values.find(v => { return String(v).replace(/\\/g, '/') === normalizedTarget; }); if (match) { targetWidget.value = match; return; } } targetWidget.value = relPath; }