vidfom's picture
Upload folder using huggingface_hub (part 5)
289daab verified
Raw
History Blame Contribute Delete
82.1 kB
/**
* 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 = `<div style="color:#aaa; text-align:center; margin-top:50px;">${t('noPreview')}</div>`;
}
};
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 = `<strong>Size:</strong> ${escapeHtml(model.size_mb)} MB` + (m.baseModel ? ` <strong style="margin-left:10px;">Base:</strong> ${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'} <span style="font-size:0.8em; opacity:0.6;">(${escapeHtml(m.baseModel)})</span>`;
const compList = document.createElement('div');
compList.className = 'anomalous-compatible-list';
compList.innerHTML = `<span style="color:#888;">${t('loadingCompatible') || 'Loading...'}</span>`;
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 = `<span style="color:#888;">No compatible models found.</span>`;
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 = `<video src="${m_comp.preview_url}" muted loop playsinline></video>`;
else thumb = `<img src="${m_comp.preview_url}" />`;
} else {
thumb = `<div style="width:30px; height:30px; background:#222; border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:10px; color:#555;">?</div>`;
}
mItem.innerHTML = `${thumb}<div class="anomalous-compatible-item-name">${escapeHtml(m_comp.filename)}</div>`;
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 = `<span style="color:#ff4444;">Failed to load.</span>`;
});
}
// ---------------------------------
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 = `<div style="color:#9aa0a6; font-size:0.9em; text-align:center;">${window.anomalous_browser_lang === 'zh' ? '暂无封面' : 'No Cover'}</div>`;
}
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 = `<span style="color:#9aa0a6;">${window.anomalous_browser_lang === 'zh' ? '文件:' : 'File:'}</span> ${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'
? `已选 <span style="color: #8ab4f8; font-size: 16px; font-weight: 500;">${total}</span> 项`
: `Selected <span style="color: #8ab4f8; font-size: 16px; font-weight: 500;">${total}</span> 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 = '<span style="color:#f28b82">Error</span>';
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 = '<span style="font-size:40px;opacity:0.2;">📄</span>';
}
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 = `<div class="status-text" style="grid-column: 1 / -1; padding: 60px; text-align: center; color: #f28b82; font-size: 16px;">❌ 请求后端 API 失败!<br><br>你必须重启 ComfyUI 进程,刚写入后端的代码才能生效。</div>`;
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 = `<div class="status-text" style="grid-column: 1 / -1; padding: 60px; text-align: center; color: #9aa0a6; font-size: 16px;">No models found.</div>`;
} 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 = `<span>🌟 ${window.anomalous_browser_lang === 'zh' ? '全部模型' : 'All Models'}</span>`;
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 = `<span style="color:#9aa0a6">📁</span> <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;">${escapeHtml(fData.name)}</span> <span style="color:#5f6368;font-size:12px">(${escapeHtml(fData.model_count)})</span>`;
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;
}