1815 lines
80 KiB
JavaScript
1815 lines
80 KiB
JavaScript
/**
|
|
* 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 (typeof this.recipeModelReturn === 'function') {
|
|
const returnToRecipe = this.recipeModelReturn;
|
|
this.recipeModelReturn = null;
|
|
returnToRecipe();
|
|
return;
|
|
}
|
|
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;
|
|
}
|