Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_sidebar.js
T

2372 lines
113 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ui_sidebar.js
* Extracted Sidebar methods.
*/
import { app } from "../../../scripts/app.js";
import { i18n } from './locales.js';
import { escapeHtml } 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 createDOM() {
localStorage.removeItem('anomalous_api_key');
localStorage.removeItem('anomalous_civitai_api_key');
this.modal = document.createElement('div');
this.modal.id = 'anomalous-modal';
const container = document.createElement('div');
container.id = 'anomalous-container';
const updateLangClass = () => {
let lang = window.anomalous_browser_lang || 'zh';
if (lang === 'en') container.classList.add('anomalous-lang-en');
else container.classList.remove('anomalous-lang-en');
};
updateLangClass();
const savedScale = localStorage.getItem('anomalous_ui_scale') || 1;
container.style.setProperty('--anomalous-scale', savedScale);
// Sidebar
this.sidebarWrapper = document.createElement('div');
this.sidebarWrapper.id = 'anomalous-sidebar-wrapper';
this.sidebarWrapper.style.position = 'relative';
const brandBar = document.createElement('div');
brandBar.style.padding = '15px 15px 10px 15px';
brandBar.style.display = 'flex';
brandBar.style.alignItems = 'center';
brandBar.style.justifyContent = 'space-between';
brandBar.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
const badge = document.createElement('div');
badge.style.background = 'linear-gradient(135deg, #444, #222)';
badge.style.color = '#ccc';
badge.style.fontSize = '0.7em';
badge.style.padding = '4px 8px';
badge.style.borderRadius = '6px';
badge.style.letterSpacing = '1px';
badge.style.border = '1px solid #555';
badge.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
badge.style.textTransform = 'uppercase';
badge.style.fontWeight = 'bold';
badge.innerHTML = 'Anomalous Browser';
const menuBtn = document.createElement('button');
menuBtn.innerHTML = '☰';
menuBtn.title = window.anomalous_browser_lang === 'zh' ? '收起/展开侧边栏' : 'Toggle Sidebar';
menuBtn.style.background = 'transparent';
menuBtn.style.border = 'none';
menuBtn.style.color = '#ccc';
menuBtn.style.fontSize = '1.2em';
menuBtn.style.cursor = 'pointer';
menuBtn.onclick = () => {
if (container.classList.contains('anomalous-sidebar-closed')) {
container.classList.remove('anomalous-sidebar-closed');
localStorage.setItem('anomalous_user_sidebar_closed', 'false');
} else {
container.classList.add('anomalous-sidebar-closed');
localStorage.setItem('anomalous_user_sidebar_closed', 'true');
}
};
brandBar.appendChild(badge);
brandBar.appendChild(menuBtn);
this.sidebar = document.createElement('div');
this.sidebar.id = 'anomalous-sidebar';
this.sidebarActions = document.createElement('div');
this.sidebarActions.id = 'anomalous-sidebar-actions';
this.sidebarActions.style.padding = '10px 15px';
this.sidebarActions.style.display = 'flex';
this.sidebarActions.style.flexDirection = 'row';
this.sidebarActions.style.justifyContent = 'flex-start';
this.sidebarActions.style.alignItems = 'center';
this.sidebarActions.style.gap = '10px';
this.sidebarActions.style.borderTop = '1px solid rgba(255,255,255,0.05)';
this.sidebarActions.style.background = 'transparent';
this.sidebarActions.style.borderRadius = '0';
this.sidebarActions.style.width = '100%';
this.sidebarActions.style.boxSizing = 'border-box';
this.sidebarActions.style.margin = '0';
this.sidebarWrapper.appendChild(brandBar);
this.sidebarWrapper.appendChild(this.sidebar);
this.sidebarWrapper.appendChild(this.sidebarActions);
// Content Area
const content = document.createElement('div');
content.id = 'anomalous-content';
const header = document.createElement('div');
header.id = 'anomalous-header';
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
const enforceBounds = (x, y) => {
let newX = x;
let newY = y;
if (newX + container.offsetWidth > window.innerWidth) newX = window.innerWidth - container.offsetWidth;
if (newY + container.offsetHeight > window.innerHeight) newY = window.innerHeight - container.offsetHeight;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
return { x: newX, y: newY };
};
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.id === 'anomalous-close') return;
isDragging = true;
const rect = container.getBoundingClientRect();
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const pos = enforceBounds(e.clientX - dragOffsetX, e.clientY - dragOffsetY);
container.style.left = pos.x + 'px';
container.style.top = pos.y + 'px';
container.style.transform = 'none';
});
window.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
localStorage.setItem('anomalous_pos_x', container.style.left);
localStorage.setItem('anomalous_pos_y', container.style.top);
}
});
const savedX = localStorage.getItem('anomalous_pos_x');
const savedY = localStorage.getItem('anomalous_pos_y');
if (savedX && savedY) {
container.style.left = savedX;
container.style.top = savedY;
}
// Periodically enforce bounds to catch resize/zoom changes
setInterval(() => {
if (!isDragging && container.style.display !== 'none' && !container.classList.contains('anomalous-docked')) {
const rect = container.getBoundingClientRect();
const pos = enforceBounds(rect.left, rect.top);
if (pos.x !== rect.left || pos.y !== rect.top) {
if (container.style.left.endsWith('px') && container.style.top.endsWith('px')) {
container.style.left = pos.x + 'px';
container.style.top = pos.y + 'px';
}
}
}
}, 1000);
const spacer = document.createElement('div');
spacer.style.flex = '1 1 auto';
const leftGroup = document.createElement('div');
leftGroup.className = 'anomalous-header-group';
const rightGroup = document.createElement('div');
rightGroup.className = 'anomalous-header-group';
// We will define hideAllPanels as a class method instead of a local closure to make it globally accessible.
const showSidebar = () => {
container.classList.remove('anomalous-sidebar-closed');
};
const modelsBtn = document.createElement('button');
modelsBtn.id = 'anomalous-models-btn';
modelsBtn.innerHTML = `🏠 <span class="anomalous-btn-text">${t('models')}</span>`;
modelsBtn.onclick = () => {
this.hideAllPanels();
if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') {
container.classList.add('anomalous-sidebar-closed');
} else {
showSidebar();
}
menuBtn.disabled = false;
menuBtn.style.opacity = '1';
menuBtn.style.cursor = 'pointer';
this.grid.style.display = 'grid';
if (this.detailPanel.innerHTML !== '') {
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
this.currentDetailModel = null;
this.historyStack = [];
}
};
const galleryBtn = document.createElement('button');
galleryBtn.innerHTML = `🖼️ <span class="anomalous-btn-text">${t('gallery') || '图库'}</span>`;
galleryBtn.onclick = () => {
this.hideAllPanels();
container.classList.add('anomalous-sidebar-closed');
menuBtn.disabled = true;
menuBtn.style.opacity = '0.3';
menuBtn.style.cursor = 'not-allowed';
this.galleryPanel.style.display = 'flex';
if (!this.galleryLoaded) {
this.loadGalleryImages(1, true);
}
};
const dockBtn = document.createElement('button');
dockBtn.innerHTML = '◧';
dockBtn.title = t('dockTitle');
dockBtn.onclick = () => {
container.classList.toggle('anomalous-docked');
if (container.classList.contains('anomalous-docked')) {
localStorage.setItem('anomalous_docked', 'true');
} else {
localStorage.setItem('anomalous_docked', 'false');
}
};
if (localStorage.getItem('anomalous_docked') === 'true') {
container.classList.add('anomalous-docked');
}
const helpBtn = document.createElement('button');
helpBtn.id = 'anomalous-help-btn';
helpBtn.title = t('helpTitle');
helpBtn.innerHTML = `❓ <span class="anomalous-btn-text">${t('help')}</span>`;
helpBtn.onclick = () => this.showHelp();
const nbBtn = document.createElement('button');
nbBtn.id = 'anomalous-notebook-btn';
nbBtn.title = t('workspaceTitle');
nbBtn.innerHTML = `📑 <span class="anomalous-btn-text">${t('workspace')}</span>`;
const dBtn = document.getElementById('anomalous-doctor-btn');
if (dBtn) dBtn.title = window.anomalous_browser_lang === 'zh' ? '模型医生' : 'Model Doctor';
const aBtn = document.getElementById('anomalous-assistant-btn');
if (aBtn) aBtn.title = window.anomalous_browser_lang === 'zh' ? '节点助手' : 'Node Assistant';
const iBtn = document.getElementById('anomalous-import-btn');
if (iBtn) iBtn.title = window.anomalous_browser_lang === 'zh' ? '📥 预检导入工作流' : '📥 Preflight Import';
const sBtn = document.getElementById('anomalous-settings-btn');
if (sBtn) sBtn.title = window.anomalous_browser_lang === 'zh' ? '设置中心' : 'Settings Hub';
// Reset dynamic panels so they re-render in new language
if (window.anomalousBrowserInstance) {
const b = window.anomalousBrowserInstance;
if (b.doctorPanel) {
b.doctorPanel.innerHTML = '';
b.doctorPanelInitialized = false;
}
}
const impOverlay = document.getElementById('anomalous-import-overlay');
if (impOverlay && impOverlay.parentNode) {
impOverlay.parentNode.removeChild(impOverlay);
}
nbBtn.onclick = () => {
this.nbPanel.style.display = 'flex';
this.showNotebooks();
};
rightGroup.appendChild(modelsBtn);
rightGroup.appendChild(galleryBtn);
rightGroup.appendChild(nbBtn);
const apiKeyBtn = document.createElement('button');
apiKeyBtn.id = 'anomalous-api-btn';
apiKeyBtn.innerHTML = `<span class="anomalous-btn-text">${t('apiKeyConfig')}</span>`;
apiKeyBtn.onclick = async () => {
const val = prompt(t('apiKeyPrompt'), '');
if (val !== null) {
try {
await fetch('/anomalous/save_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: val.trim() })
});
} catch (e) { }
}
};
const scanBtn = document.createElement('button');
scanBtn.id = 'anomalous-scan-btn';
scanBtn.title = window.anomalous_browser_lang === 'zh' ? '🔄 扫描向导 (Scan Wizard)' : '🔄 Scan Wizard';
scanBtn.innerHTML = `🔄`;
scanBtn.style.background = 'transparent';
scanBtn.style.color = '#ccc';
scanBtn.style.border = 'none';
scanBtn.style.borderRadius = '6px';
scanBtn.style.padding = '6px';
scanBtn.style.fontSize = '1.1em';
scanBtn.style.cursor = 'pointer';
scanBtn.style.transition = 'all 0.2s ease';
scanBtn.onmouseover = () => { scanBtn.style.background = 'rgba(255,255,255,0.1)'; scanBtn.style.color = '#fff'; };
scanBtn.onmouseout = () => { scanBtn.style.background = 'transparent'; scanBtn.style.color = '#ccc'; };
let isCurrentlyScanning = false;
setInterval(async () => {
try {
let isScanning = false;
if (this.currentType) {
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx || 0, subfolder: this.currentSubfolder || '/' });
const resLocal = await fetch('/anomalous/scan_status?' + params.toString());
const dataLocal = await resLocal.json();
if (dataLocal.scanning) isScanning = true;
}
const resGlobal = await fetch('/anomalous/global_scan_status');
const dataGlobal = await resGlobal.json();
if (dataGlobal.scanning) isScanning = true;
if (isScanning && !isCurrentlyScanning) {
isCurrentlyScanning = true;
scanBtn.innerHTML = `⏳`;
scanBtn.style.opacity = '0.7';
} else if (!isScanning && isCurrentlyScanning) {
isCurrentlyScanning = false;
scanBtn.innerHTML = `🔄`;
scanBtn.style.opacity = '1';
this.loadModels();
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
alert(window.anomalous_browser_lang === 'zh' ? '✅ 扫描/重命名已完成!数据已为您更新。' : '✅ Scan/Rename completed! Data updated.');
}
} catch (e) { }
}, 3000);
const createWizardModal = (isGlobal = false, targetFiles = null) => {
let wizard = document.getElementById('anomalous-wizard-modal');
if (wizard) document.body.removeChild(wizard);
wizard = document.createElement('div');
wizard.id = 'anomalous-wizard-modal';
wizard.style.position = 'fixed';
wizard.style.top = '0';
wizard.style.left = '0';
wizard.style.width = '100vw';
wizard.style.height = '100vh';
wizard.style.backgroundColor = 'rgba(0,0,0,0.6)';
wizard.style.zIndex = '999999';
wizard.style.display = 'flex';
wizard.style.justifyContent = 'center';
wizard.style.alignItems = 'center';
wizard.style.fontFamily = 'Roboto, "Segoe UI", sans-serif';
const content = document.createElement('div');
content.style.background = '#1E1E1E';
content.style.borderRadius = '12px';
content.style.padding = '32px';
content.style.width = '760px';
content.style.maxWidth = '95%';
content.style.maxHeight = '90vh';
content.style.overflowY = 'auto';
content.style.boxShadow = '0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12)';
content.style.color = '#fff';
content.style.position = 'relative';
// Top Header Area
const headerArea = document.createElement('div');
headerArea.style.display = 'flex';
headerArea.style.justifyContent = 'space-between';
headerArea.style.alignItems = 'center';
headerArea.style.marginBottom = '24px';
const title = document.createElement('h2');
if (targetFiles) {
title.innerText = window.anomalous_browser_lang === 'zh'
? `🎯 单模型扫描向导: ${targetFiles}`
: `🎯 Single Model Scan: ${targetFiles}`;
} else {
title.innerText = window.anomalous_browser_lang === 'zh'
? (isGlobal ? '全局扫描向导' : '扫描向导')
: (isGlobal ? 'Global Scan Wizard' : 'Scan Wizard');
}
title.style.margin = '0';
title.style.fontSize = '1.6em';
title.style.fontWeight = '500';
// Toolbar right
const topToolbar = document.createElement('div');
topToolbar.style.display = 'flex';
topToolbar.style.gap = '8px';
const createGhostBtn = (icon, textZh, textEn, onClick) => {
const btn = document.createElement('button');
btn.innerHTML = `${icon} ${window.anomalous_browser_lang === 'zh' ? textZh : textEn}`;
btn.style.padding = '6px 12px';
btn.style.background = 'transparent';
btn.style.color = '#ccc';
btn.style.border = '1px solid rgba(255,255,255,0.1)';
btn.style.borderRadius = '6px';
btn.style.cursor = 'pointer';
btn.style.fontSize = '0.85em';
btn.style.transition = 'all 0.2s';
btn.onmouseover = () => { btn.style.background = 'rgba(255,255,255,0.08)'; btn.style.color = '#fff'; };
btn.onmouseout = () => { btn.style.background = 'transparent'; btn.style.color = '#ccc'; };
btn.onclick = onClick;
return btn;
};
const apiKeyBtn = createGhostBtn('🔑', 'API 密钥', 'API Key', () => {
const newKey = prompt(window.anomalous_browser_lang === 'zh' ? '请输入 Civitai API Key(留空将清除):' : 'Enter Civitai API Key (leave blank to clear):', '');
if (newKey !== null) {
fetch('/anomalous/save_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: newKey.trim() })
}).then(res => res.json()).then(data => {
if (data.status !== 'ok') throw new Error(data.message || 'Save failed');
alert(window.anomalous_browser_lang === 'zh' ? '✅ 密钥保存成功' : '✅ Key Saved');
}).catch(error => alert((window.anomalous_browser_lang === 'zh' ? '❌ 保存失败: ' : '❌ Save failed: ') + error.message));
}
});
topToolbar.appendChild(apiKeyBtn);
headerArea.appendChild(title);
headerArea.appendChild(topToolbar);
content.appendChild(headerArea);
let scanMode = 'civitai';
let enableRename = true;
let enableAutoCheck = true;
let enableForceOverwrite = false;
let updateSections = () => { };
const formGroup = document.createElement('div');
formGroup.style.display = 'flex';
formGroup.style.flexDirection = 'column';
formGroup.style.gap = '24px';
const createChoiceCard = (id, icon, titleZh, titleEn, descZh, descEn, isSelected) => {
const card = document.createElement('div');
card.style.flex = '1';
card.style.background = isSelected ? 'rgba(138, 180, 248, 0.15)' : 'rgba(255, 255, 255, 0.05)';
card.style.border = `2px solid ${isSelected ? '#8AB4F8' : 'transparent'}`;
card.style.borderRadius = '8px';
card.style.padding = '16px';
card.style.cursor = 'pointer';
card.style.transition = 'all 0.2s';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.gap = '12px';
card.style.alignItems = 'flex-start';
card.onmouseover = () => {
if (!isSelected) card.style.background = 'rgba(255, 255, 255, 0.08)';
};
card.onmouseout = () => {
if (!isSelected) card.style.background = 'rgba(255, 255, 255, 0.05)';
};
const topRow = document.createElement('div');
topRow.style.display = 'flex';
topRow.style.alignItems = 'center';
topRow.style.gap = '12px';
const iconDiv = document.createElement('div');
iconDiv.innerText = icon;
iconDiv.style.fontSize = '1.6em';
const tTitle = document.createElement('div');
tTitle.innerText = window.anomalous_browser_lang === 'zh' ? titleZh : titleEn;
tTitle.style.fontWeight = '500';
tTitle.style.fontSize = '1.05em';
tTitle.style.color = isSelected ? '#8AB4F8' : '#fff';
topRow.appendChild(iconDiv);
topRow.appendChild(tTitle);
const tDesc = document.createElement('div');
tDesc.innerText = window.anomalous_browser_lang === 'zh' ? descZh : descEn;
tDesc.style.fontSize = '0.85em';
tDesc.style.color = '#bbb';
tDesc.style.lineHeight = '1.5';
card.appendChild(topRow);
card.appendChild(tDesc);
return card;
};
let targetMode = 'all';
let selectedForScan = new Map();
if (!targetFiles) {
const section0 = document.createElement('div');
const sec0Title = document.createElement('div');
sec0Title.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 0. 扫描目标' : 'Step 0. Scan Target';
sec0Title.style.fontWeight = '500';
sec0Title.style.color = '#8AB4F8';
sec0Title.style.marginBottom = '12px';
sec0Title.style.fontSize = '0.95em';
section0.appendChild(sec0Title);
const tContainer = document.createElement('div');
tContainer.style.display = 'flex';
tContainer.style.flexDirection = 'row';
tContainer.style.gap = '16px';
tContainer.style.marginBottom = '16px';
const customActionDiv = document.createElement('div');
customActionDiv.style.display = 'none';
customActionDiv.style.marginTop = '16px';
const openSelectorBtn = document.createElement('button');
openSelectorBtn.innerText = window.anomalous_browser_lang === 'zh' ? '📂 打开高级模型选择面板' : '📂 Open Advanced Model Selector';
openSelectorBtn.style.cssText = 'width:100%;padding:12px;background:#8AB4F8;color:#1E1E1E;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:1.05em;box-shadow:0 2px 4px rgba(0,0,0,0.2);';
const selectedCountSpan = document.createElement('div');
selectedCountSpan.style.cssText = 'text-align:center;color:#8AB4F8;margin-top:8px;font-size:0.9em;';
selectedCountSpan.innerText = window.anomalous_browser_lang === 'zh' ? '已选择 0 个模型' : '0 models selected';
const updateSelectedCount = () => {
let total = 0;
for (const set of selectedForScan.values()) total += set.size;
selectedCountSpan.innerText = window.anomalous_browser_lang === 'zh' ? `已选择 ${total} 个模型` : `${total} models selected`;
};
openSelectorBtn.onclick = () => {
this._openAdvancedModelSelector(selectedForScan, (newSelection) => {
selectedForScan = newSelection;
updateSelectedCount();
});
};
customActionDiv.appendChild(openSelectorBtn);
customActionDiv.appendChild(selectedCountSpan);
let tCard1, tCard2;
const updateTargetCards = () => {
if (tCard1 && tCard2) {
tContainer.removeChild(tCard1);
tContainer.removeChild(tCard2);
}
tCard1 = createChoiceCard('all', '📂', '默认全局扫描', 'Default Global Scan', '扫描系统中所有的模型文件,自动匹配封面和信息', 'Scan all models in the system for covers and metadata', targetMode === 'all');
tCard2 = createChoiceCard('custom', '☑️', '高级自定义多选', 'Advanced Selection', '跨目录选择特定模型,启动按序队列扫描', 'Select specific models across folders, launches sequential scan', targetMode === 'custom');
tCard1.onclick = () => {
targetMode = 'all';
updateTargetCards();
customActionDiv.style.display = 'none';
};
tCard2.onclick = () => {
targetMode = 'custom';
updateTargetCards();
customActionDiv.style.display = 'block';
};
tContainer.appendChild(tCard1);
tContainer.appendChild(tCard2);
};
updateTargetCards();
section0.appendChild(tContainer);
section0.appendChild(customActionDiv);
formGroup.appendChild(section0);
}
// === Step 1: Fetch Data ===
const section1 = document.createElement('div');
const sec1Title = document.createElement('div');
sec1Title.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 1. 获取数据' : 'Step 1. Fetch Data';
sec1Title.style.fontWeight = '500';
sec1Title.style.color = '#8AB4F8';
sec1Title.style.marginBottom = '12px';
sec1Title.style.fontSize = '0.95em';
section1.appendChild(sec1Title);
const cardsContainer = document.createElement('div');
cardsContainer.style.display = 'flex';
cardsContainer.style.flexDirection = 'row';
cardsContainer.style.gap = '16px';
let card1, card2;
const updateCards = () => {
if (card1 && card2) {
cardsContainer.removeChild(card1);
cardsContainer.removeChild(card2);
}
card1 = createChoiceCard('civitai', '🌍', '在线完整匹配', 'Online Full Matching', '自动连接 Civitai 平台,深度获取模型封面图、触发词 (Trigger Words)、作者备注及详细标签。推荐日常使用。', 'Connects to Civitai to fetch comprehensive model covers, trigger words, author notes, and tags. Recommended for daily use.', scanMode === 'civitai');
card2 = createChoiceCard('offline', '🔌', '离线极速读取', 'Offline Fast Read', '不消耗任何网络。仅通过本地张量计算极速提取模型内的基础元数据,速度极快但信息较基础。', 'No network required. Extremely fast extraction of basic metadata via local tensor computation, but info is limited.', scanMode === 'offline');
card1.onclick = () => { scanMode = 'civitai'; updateCards(); updateSections(); };
card2.onclick = () => { scanMode = 'offline'; updateCards(); updateSections(); };
cardsContainer.appendChild(card1);
cardsContainer.appendChild(card2);
};
updateCards();
section1.appendChild(cardsContainer);
formGroup.appendChild(section1);
// Material Switch Builder
const createMaterialSwitch = (initialState, onChange) => {
const track = document.createElement('div');
track.style.width = '36px';
track.style.height = '14px';
track.style.borderRadius = '7px';
track.style.background = initialState ? 'rgba(138, 180, 248, 0.5)' : 'rgba(255,255,255,0.3)';
track.style.position = 'relative';
track.style.cursor = 'pointer';
track.style.transition = 'background 0.3s';
track.style.display = 'flex';
track.style.alignItems = 'center';
const thumb = document.createElement('div');
thumb.style.width = '20px';
thumb.style.height = '20px';
thumb.style.borderRadius = '50%';
thumb.style.background = initialState ? '#8AB4F8' : '#bdbdbd';
thumb.style.position = 'absolute';
thumb.style.left = initialState ? '16px' : '0px';
thumb.style.transition = 'left 0.3s, background 0.3s';
thumb.style.boxShadow = '0 1px 3px rgba(0,0,0,0.4)';
let state = initialState;
track.onclick = () => {
state = !state;
track.style.background = state ? 'rgba(138, 180, 248, 0.5)' : 'rgba(255,255,255,0.3)';
thumb.style.background = state ? '#8AB4F8' : '#bdbdbd';
thumb.style.left = state ? '16px' : '0px';
onChange(state);
};
track.appendChild(thumb);
return track;
};
const createListRow = (icon, titleZh, titleEn, descZh, descEn, actionEl) => {
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '12px 0';
row.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
const left = document.createElement('div');
left.style.display = 'flex';
left.style.alignItems = 'flex-start';
left.style.gap = '16px';
const iconEl = document.createElement('div');
iconEl.innerText = icon;
iconEl.style.fontSize = '1.4em';
iconEl.style.lineHeight = '1.2';
iconEl.style.width = '24px';
iconEl.style.textAlign = 'center';
const textDiv = document.createElement('div');
const t = document.createElement('div');
t.innerText = window.anomalous_browser_lang === 'zh' ? titleZh : titleEn;
t.style.fontWeight = '500';
t.style.fontSize = '1.0em';
t.style.color = '#fff';
const d = document.createElement('div');
d.innerHTML = window.anomalous_browser_lang === 'zh' ? descZh : descEn;
d.style.fontSize = '0.85em';
d.style.color = '#aaa';
d.style.marginTop = '4px';
textDiv.appendChild(t);
textDiv.appendChild(d);
left.appendChild(iconEl);
left.appendChild(textDiv);
row.appendChild(left);
if (actionEl) row.appendChild(actionEl);
return row;
};
// === Step 2: Normalize Naming ===
const section2 = document.createElement('div');
const sec2Title = document.createElement('div');
sec2Title.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 2. 规范命名' : 'Step 2. Normalize Naming';
sec2Title.style.fontWeight = '500';
sec2Title.style.color = '#8AB4F8';
sec2Title.style.marginBottom = '8px';
sec2Title.style.fontSize = '0.95em';
section2.appendChild(sec2Title);
const s2List = document.createElement('div');
let enableVirtualRename = true;
let enablePhysicalRename = false;
const dualChannelRow = document.createElement('div');
dualChannelRow.style.display = 'flex';
dualChannelRow.style.flexDirection = 'column';
dualChannelRow.style.gap = '14px';
dualChannelRow.style.padding = '0 16px 12px 16px';
dualChannelRow.style.marginLeft = '40px';
const updateDualChannelUI = () => {
dualChannelRow.style.opacity = enableRename ? '1' : '0.4';
dualChannelRow.style.pointerEvents = enableRename ? 'auto' : 'none';
};
const virtualSwitch = createMaterialSwitch(enableVirtualRename, (s) => enableVirtualRename = s);
const physicalSwitch = createMaterialSwitch(enablePhysicalRename, (s) => enablePhysicalRename = s);
const vContainer = document.createElement('div');
vContainer.style.display = 'flex';
vContainer.style.flexDirection = 'column';
const vRow = document.createElement('div');
vRow.style.display = 'flex';
vRow.style.alignItems = 'center';
vRow.style.gap = '8px';
vRow.innerHTML = `<span style="font-size:0.9em; color:#ddd;">✨ ${window.anomalous_browser_lang === 'zh' ? '虚拟重命名 (安全推荐)' : 'Virtual Rename (Safe)'}</span>`;
vRow.appendChild(virtualSwitch);
const vDesc = document.createElement('div');
vDesc.style.fontSize = '0.8em';
vDesc.style.color = '#888';
vDesc.style.marginTop = '4px';
vDesc.innerHTML = window.anomalous_browser_lang === 'zh' ? '仅将标准名称注入浏览器插件中,不修改硬盘上的真实文件名,随时可无损还原。' : 'Inject standard names into the browser only, without modifying actual files on disk. Safely reversible.';
vContainer.appendChild(vRow);
vContainer.appendChild(vDesc);
const pContainer = document.createElement('div');
pContainer.style.display = 'flex';
pContainer.style.flexDirection = 'column';
const pRow = document.createElement('div');
pRow.style.display = 'flex';
pRow.style.alignItems = 'center';
pRow.style.gap = '8px';
pRow.innerHTML = `<span style="font-size:0.9em; color:#ddd;">💾 ${window.anomalous_browser_lang === 'zh' ? '物理重命名' : 'Physical Rename'}</span>`;
pRow.appendChild(physicalSwitch);
const pDesc = document.createElement('div');
pDesc.style.fontSize = '0.8em';
pDesc.style.color = '#888';
pDesc.style.marginTop = '4px';
pDesc.innerHTML = window.anomalous_browser_lang === 'zh' ? '真实修改底层的 <code>.safetensors</code> 及其所有附属文件名,彻底告别乱码文件名。' : 'Permanently rename the underlying <code>.safetensors</code> and associated files on disk.';
pContainer.appendChild(pRow);
pContainer.appendChild(pDesc);
dualChannelRow.appendChild(vContainer);
dualChannelRow.appendChild(pContainer);
s2List.appendChild(createListRow('📝', '自动规范命名', 'Auto-Normalize Naming', '强迫症福音。自动将杂乱无章的模型文件名重命名为 <b>模型名_版本名.safetensors</b> 的标准格式。<br><span style="color:#8AB4F8; display:inline-block; margin-top:4px;">例如:</span> <span style="color:#ff6b6b; text-decoration:line-through;">model_final(1).safetensors</span> ➔ <span style="color:#28a745;">Beautiful_Mix_V1.0.safetensors</span>', 'Automatically rename messy model files to the official standard format: <b>ModelName_VersionName.safetensors</b>.<br><span style="color:#8AB4F8; display:inline-block; margin-top:4px;">e.g. </span> <span style="color:#ff6b6b; text-decoration:line-through;">model_final(1).safetensors</span> ➔ <span style="color:#28a745;">Beautiful_Mix_V1.0.safetensors</span>', createMaterialSwitch(enableRename, (s) => { enableRename = s; updateDualChannelUI(); })));
s2List.lastChild.style.borderBottom = 'none';
s2List.appendChild(dualChannelRow);
updateDualChannelUI();
section2.appendChild(s2List);
formGroup.appendChild(section2);
// === Step 3: Workflow Protection & Fix ===
const section3 = document.createElement('div');
const sec3Title = document.createElement('div');
sec3Title.innerText = window.anomalous_browser_lang === 'zh' ? 'Step 3. 工作流保障与修复' : 'Step 3. Workflow Protection & Fix';
sec3Title.style.fontWeight = '500';
sec3Title.style.color = '#8AB4F8';
sec3Title.style.marginBottom = '8px';
sec3Title.style.fontSize = '0.95em';
section3.appendChild(sec3Title);
const s3List = document.createElement('div');
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
s3List.appendChild(createListRow('📦', '模型溯源绑定', 'Model Provenance Binding', '全局生效。每次生成图片或保存工作流时,将模型的精确哈希值隐式写入到图像元数据与工作流 JSON 中,防止未来模型改名或换环境后报错找不到。', 'Global effect. Implicitly embed exact model hashes into generated images and saved workflows, ensuring missing models can always be auto-fixed.', createMaterialSwitch(isInject, (s) => localStorage.setItem('anomalous_inject_hash', s ? 'true' : 'false'))));
s3List.appendChild(createListRow('⚠️', '强制覆盖已有配置', 'Force Overwrite Configs', '扫描时忽略本地已存在的 .info 文件,强制重新计算哈希并覆盖下载。用于修复匹配错乱的模型。', 'Ignore existing .info files and force re-scan/overwrite. Useful for fixing mismatched models.', createMaterialSwitch(enableForceOverwrite, (s) => enableForceOverwrite = s)));
s3List.appendChild(createListRow('🪄', '智能修复当前工作流', 'Smart Fix Current Workflow', '扫描完成后,自动尝试用最新数据修复当前画布中提示缺失的报错模型节点。', 'After scanning, automatically attempt to fix missing model errors on the current canvas.', createMaterialSwitch(enableAutoCheck, (s) => enableAutoCheck = s)));
s3List.lastChild.style.borderBottom = 'none';
section3.appendChild(s3List);
formGroup.appendChild(section3);
content.appendChild(formGroup);
updateSections = () => {
if (scanMode === 'offline') {
section2.style.opacity = '0.3';
section2.style.pointerEvents = 'none';
section3.style.opacity = '0.3';
section3.style.pointerEvents = 'none';
} else {
section2.style.opacity = '1';
section2.style.pointerEvents = 'auto';
section3.style.opacity = '1';
section3.style.pointerEvents = 'auto';
}
};
updateSections();
// Execute Scan Logic
const doScan = async () => {
try {
const reqBody = {
offline_only: scanMode === 'offline',
skip_rename: !enableRename,
virtual_rename: enableRename ? enableVirtualRename : false,
physical_rename: enableRename ? enablePhysicalRename : false,
force_overwrite: enableForceOverwrite
};
if (targetMode === 'custom') {
let hasAny = false;
for (const set of selectedForScan.values()) {
if (set.size > 0) hasAny = true;
}
if (!hasAny) {
alert(window.anomalous_browser_lang === 'zh' ? '未选择任何模型' : 'No models selected');
return;
}
document.body.removeChild(wizard);
if (typeof scanBtn !== 'undefined') {
scanBtn.innerHTML = `⏳`;
scanBtn.style.animation = 'anomalous-spin 2s linear infinite';
}
// Sequential scan for multiple folders
for (const [folderKey, fileSet] of selectedForScan.entries()) {
if (fileSet.size === 0) continue;
const parts = folderKey.split('|');
if (parts.length < 3) continue;
const type = parts[0], path_idx = parts[1], subfolder = parts.slice(2).join('|');
const params = new URLSearchParams({ type, path_idx, subfolder });
const targetUrl = '/anomalous/scan?' + params.toString();
const currentReqBody = { ...reqBody, target_files: Array.from(fileSet) };
try {
const res = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentReqBody)
});
const data = await res.json();
if (data.status === 'ok') {
// wait for this folder to finish scanning before starting next
await new Promise(resolve => {
const poll = setInterval(async () => {
try {
const statusUrl = '/anomalous/scan_status?' + params.toString();
const statusRes = await fetch(statusUrl);
const statusData = await statusRes.json();
if (!statusData.scanning) {
clearInterval(poll);
resolve();
}
} catch (err) {
clearInterval(poll);
resolve();
}
}, 2000);
});
} else {
console.error("Scan failed for folder: " + folderKey, data.message);
}
} catch(e) {
console.error("Request failed for folder: " + folderKey, e);
}
}
if (typeof scanBtn !== 'undefined') {
scanBtn.innerHTML = `🔄`;
scanBtn.style.animation = '';
}
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
window.anomalous_resolve_all_missing_nodes(true);
}
this.loadModels();
return;
}
// Existing logic for 'all' mode or single file
let finalTargetFiles = targetFiles;
let currentReqBody = { ...reqBody };
let targetUrl = '';
if (isGlobal && targetMode !== 'custom') {
targetUrl = '/anomalous/scan_all';
} else {
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
if (finalTargetFiles) {
currentReqBody.target_files = finalTargetFiles.split(',');
}
targetUrl = '/anomalous/scan?' + params.toString();
}
const res = await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(currentReqBody)
});
const data = await res.json();
if (data.status === 'ok') {
if (typeof scanBtn !== 'undefined') {
scanBtn.innerHTML = `⏳`;
scanBtn.style.animation = 'anomalous-spin 2s linear infinite';
}
// Start polling
const poll = setInterval(async () => {
try {
let statusUrl = '/anomalous/global_scan_status';
if (!isGlobal) {
const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder });
statusUrl = '/anomalous/scan_status?' + params.toString();
}
const statusRes = await fetch(statusUrl);
const statusData = await statusRes.json();
if (!statusData.scanning) {
clearInterval(poll);
if (typeof scanBtn !== 'undefined') {
scanBtn.innerHTML = `🔄`;
scanBtn.style.animation = '';
}
if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) {
window.anomalous_resolve_all_missing_nodes(true);
}
this.loadModels();
}
} catch (err) {
clearInterval(poll);
if (typeof scanBtn !== 'undefined') {
scanBtn.innerHTML = `🔄`;
scanBtn.style.animation = '';
}
}
}, 2000);
document.body.removeChild(wizard);
return; // return early so we don't remove wizard again below
} else {
alert(window.anomalous_browser_lang === 'zh' ? '扫描失败: ' + data.message : 'Scan failed: ' + data.message);
}
} catch (e) { alert("Error: " + e); }
document.body.removeChild(wizard);
};
const footer = document.createElement('div');
footer.style.marginTop = '32px';
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '8px';
const closeBtn = document.createElement('button');
closeBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel';
closeBtn.style.padding = '8px 16px';
closeBtn.style.background = 'transparent';
closeBtn.style.color = '#8AB4F8';
closeBtn.style.border = 'none';
closeBtn.style.borderRadius = '4px';
closeBtn.style.cursor = 'pointer';
closeBtn.style.fontSize = '0.95em';
closeBtn.style.fontWeight = '500';
closeBtn.style.textTransform = 'uppercase';
closeBtn.style.transition = 'background 0.2s';
closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(138, 180, 248, 0.08)';
closeBtn.onmouseout = () => closeBtn.style.background = 'transparent';
closeBtn.onclick = () => document.body.removeChild(wizard);
const startBtn = document.createElement('button');
startBtn.innerText = window.anomalous_browser_lang === 'zh' ? '执行扫描' : 'Execute Scan';
startBtn.style.padding = '8px 24px';
startBtn.style.background = '#8AB4F8'; // Material Primary
startBtn.style.color = '#1E1E1E';
startBtn.style.border = 'none';
startBtn.style.borderRadius = '4px';
startBtn.style.cursor = 'pointer';
startBtn.style.fontSize = '0.95em';
startBtn.style.fontWeight = '500';
startBtn.style.textTransform = 'uppercase';
startBtn.style.transition = 'background 0.2s, box-shadow 0.2s';
startBtn.style.boxShadow = '0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)';
startBtn.onmouseover = () => {
startBtn.style.background = '#aecbf9';
startBtn.style.boxShadow = '0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12)';
};
startBtn.onmouseout = () => {
startBtn.style.background = '#8AB4F8';
startBtn.style.boxShadow = '0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)';
};
startBtn.onclick = doScan;
footer.appendChild(closeBtn);
footer.appendChild(startBtn);
content.appendChild(footer);
wizard.appendChild(content);
document.body.appendChild(wizard);
};
scanBtn.onclick = () => createWizardModal(true);
this.sidebarActions.appendChild(scanBtn);
let refreshModelSettingsText = () => {};
const closeBtn = document.createElement('div');
closeBtn.id = 'anomalous-close';
closeBtn.innerHTML = '&times;';
closeBtn.onclick = () => this.close();
rightGroup.appendChild(dockBtn);
rightGroup.appendChild(closeBtn);
header.appendChild(leftGroup);
header.appendChild(spacer);
header.appendChild(rightGroup);
const settingsHubModal = document.createElement('div');
settingsHubModal.style.position = 'absolute';
settingsHubModal.style.bottom = '15px';
settingsHubModal.style.left = '100%';
settingsHubModal.style.marginLeft = '10px';
settingsHubModal.style.width = '260px';
settingsHubModal.style.background = 'var(--comfy-menu-bg, #2a2a2a)';
settingsHubModal.style.border = '1px solid rgba(255,255,255,0.1)';
settingsHubModal.style.borderRadius = '12px';
settingsHubModal.style.padding = '10px';
settingsHubModal.style.display = 'none';
settingsHubModal.style.flexDirection = 'column';
settingsHubModal.style.gap = '4px';
settingsHubModal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.5)';
settingsHubModal.style.zIndex = '1000';
const langBtn = document.createElement('button');
langBtn.className = 'anomalous-lang-btn';
langBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🌐 Language: EN' : '🌐 语言: 中文';
langBtn.onclick = () => {
let newLang = window.anomalous_browser_lang === 'zh' ? 'en' : 'zh';
localStorage.setItem('anomalous_lang', newLang);
window.anomalous_browser_lang = newLang;
langBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🌐 Language: EN' : '🌐 语言: 中文';
updateLangClass();
modelsBtn.innerHTML = `🏠 <span class="anomalous-btn-text">${t('models')}</span>`;
galleryBtn.innerHTML = `🖼️ <span class="anomalous-btn-text">${t('gallery')}</span>`;
scanBtn.title = t('scanTitle');
scanBtn.innerHTML = `🔄`;
helpBtn.innerHTML = `❓ <span class="anomalous-btn-text">${t('help')}</span>`;
nbBtn.title = t('workspaceTitle');
nbBtn.innerHTML = `📑 <span class="anomalous-btn-text">${t('workspace')}</span>`;
const dBtn = document.getElementById('anomalous-doctor-btn');
if (dBtn) dBtn.title = window.anomalous_browser_lang === 'zh' ? '模型医生' : 'Model Doctor';
const aBtn = document.getElementById('anomalous-assistant-btn');
if (aBtn) aBtn.title = window.anomalous_browser_lang === 'zh' ? '节点助手' : 'Node Assistant';
const iBtn = document.getElementById('anomalous-import-btn');
if (iBtn) iBtn.title = window.anomalous_browser_lang === 'zh' ? '📥 导入 / 导出工作流 (分享码)' : '📥 Import / Export Workflow (Share Code)';
const sBtn = document.getElementById('anomalous-global-settings-btn');
if (sBtn) sBtn.title = window.anomalous_browser_lang === 'zh' ? '设置中心' : 'Settings Hub';
// Reset dynamic panels so they re-render in new language
if (window.anomalousBrowserInstance) {
const b = window.anomalousBrowserInstance;
if (b.doctorPanel) {
b.doctorPanel.innerHTML = '';
b.doctorPanelInitialized = false;
}
if (b.notebookNotesTab) b.notebookNotesTab.textContent = t('promptNotes');
if (b.notebookRecipesTab) b.notebookRecipesTab.textContent = t('recipeTitle');
}
const impOverlay = document.getElementById('anomalous-import-overlay');
if (impOverlay && impOverlay.parentNode) {
impOverlay.parentNode.removeChild(impOverlay);
}
apiKeyBtn.innerHTML = `<span class="anomalous-btn-text">${t('apiKeyConfig')}</span>`;
const globalScanBtnRef = document.getElementById('anomalous-global-scan-btn');
if (globalScanBtnRef) globalScanBtnRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '🌍 一键全盘极速扫描 (不下载封面/不改名)' : '🌍 Global Quick Scan (No rename/No media)';
const checkUnscannedBtnRef = document.getElementById('anomalous-check-unscanned-btn');
if (checkUnscannedBtnRef) checkUnscannedBtnRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info';
const resetBtnRef = document.getElementById('anomalous-reset-btn');
if (resetBtnRef) resetBtnRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔄 重置界面布局' : '🔄 Reset Layout';
const scaleLabelRef = document.getElementById('anomalous-scale-label');
if (scaleLabelRef) scaleLabelRef.innerText = window.anomalous_browser_lang === 'zh' ? 'UI 缩放' : 'UI Scale';
const hashBtnRef = document.getElementById('anomalous-hash-toggle-btn');
if (hashBtnRef) {
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
hashBtnRef.innerHTML = window.anomalous_browser_lang === 'zh'
? (isInject ? '🟢 注入工作流哈希' : '⚪ 不注入工作流哈希')
: (isInject ? '🟢 Inject Workflow Hash' : '⚪ Skip Workflow Hash');
}
const folderMgrRef = document.getElementById('anomalous-folder-manager-btn');
if (folderMgrRef) folderMgrRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '📁 文件夹管理' : '📁 Manage Folders';
const feedbackRef = document.getElementById('anomalous-feedback-btn');
if (feedbackRef) feedbackRef.innerHTML = window.anomalous_browser_lang === 'zh' ? '💬 提交反馈 / 报告问题' : '💬 Submit Feedback / Report Bug';
refreshModelSettingsText();
this.renderSidebar();
this.loadModels();
if (this.detailPanel.style.display !== 'none' && this.currentDetailModel) {
this.showDetail(this.currentDetailModel);
}
if (this.nbEditor && this.nbEditor.innerHTML !== '') {
this.renderNotebookEditor();
this.refreshNotebooks();
}
};
const styleHubBtn = (btn) => {
btn.style.background = 'transparent';
btn.style.border = '1px solid rgba(255,255,255,0.05)';
btn.style.color = '#ccc';
btn.style.textAlign = 'left';
btn.style.padding = '8px 10px';
btn.style.borderRadius = '8px';
btn.style.cursor = 'pointer';
btn.style.fontSize = '0.85em';
btn.style.transition = 'all 0.2s';
btn.onmouseover = () => { btn.style.background = 'rgba(255,255,255,0.08)'; btn.style.color = '#fff'; };
btn.onmouseout = () => { btn.style.background = 'transparent'; btn.style.color = '#ccc'; };
};
styleHubBtn(apiKeyBtn);
styleHubBtn(langBtn);
styleHubBtn(helpBtn);
const modelSettingsBtn = document.createElement('button');
modelSettingsBtn.id = 'anomalous-model-settings-btn';
styleHubBtn(modelSettingsBtn);
const modelSettingsOverlay = document.createElement('div');
modelSettingsOverlay.className = 'anomalous-model-settings-overlay';
modelSettingsOverlay.hidden = true;
const modelSettingsDialog = document.createElement('div');
modelSettingsDialog.className = 'anomalous-model-settings-dialog';
modelSettingsDialog.setAttribute('role', 'dialog');
modelSettingsDialog.setAttribute('aria-modal', 'true');
const modelSettingsTitle = document.createElement('h2');
const modelSettingsDescription = document.createElement('p');
modelSettingsDescription.className = 'anomalous-model-settings-description';
const createSettingRow = () => {
const row = document.createElement('label');
row.className = 'anomalous-model-setting-row';
const copy = document.createElement('span');
copy.className = 'anomalous-model-setting-copy';
const name = document.createElement('strong');
const help = document.createElement('small');
copy.append(name, help);
const select = document.createElement('select');
select.className = 'anomalous-model-setting-select';
row.append(copy, select);
return { row, name, help, select };
};
const videoSetting = createSettingRow();
const alwaysPlayOption = new Option('', 'always');
const hoverPlayOption = new Option('', 'hover');
videoSetting.select.append(alwaysPlayOption, hoverPlayOption);
const thumbnailSetting = createSettingRow();
const balancedThumbnailOption = new Option('', 'balanced');
const originalThumbnailOption = new Option('', 'original');
thumbnailSetting.select.append(balancedThumbnailOption, originalThumbnailOption);
const modelSettingsNote = document.createElement('div');
modelSettingsNote.className = 'anomalous-model-settings-note';
const modelSettingsClose = document.createElement('button');
modelSettingsClose.className = 'anomalous-model-settings-close';
modelSettingsDialog.append(
modelSettingsTitle,
modelSettingsDescription,
videoSetting.row,
thumbnailSetting.row,
modelSettingsNote,
modelSettingsClose,
);
modelSettingsOverlay.appendChild(modelSettingsDialog);
container.appendChild(modelSettingsOverlay);
refreshModelSettingsText = () => {
const zh = window.anomalous_browser_lang === 'zh';
modelSettingsBtn.textContent = zh ? '🖼️ 模型设置' : '🖼️ Model Settings';
modelSettingsTitle.textContent = zh ? '模型卡片设置' : 'Model Card Settings';
modelSettingsDescription.textContent = zh
? '只调整模型浏览器里的卡片显示,不会修改模型文件或原始封面。'
: 'Only changes cards in the model browser. Model files and original covers remain untouched.';
videoSetting.name.textContent = zh ? '视频封面播放' : 'Video cover playback';
videoSetting.help.textContent = zh
? '始终播放更有动感;悬停播放更省显存与电量。'
: 'Always play is more lively; hover play uses less GPU memory and power.';
alwaysPlayOption.textContent = zh ? '始终播放' : 'Always play';
hoverPlayOption.textContent = zh ? '悬停播放(推荐)' : 'Play on hover (Recommended)';
thumbnailSetting.name.textContent = zh ? '卡片图片清晰度' : 'Card image quality';
thumbnailSetting.help.textContent = zh
? '流畅缩略图只用于卡片;打开详情仍显示原始封面。'
: 'Optimized thumbnails are card-only; details still show the original cover.';
balancedThumbnailOption.textContent = zh ? '流畅缩略图(推荐)' : 'Optimized thumbnail (Recommended)';
originalThumbnailOption.textContent = zh ? '原始封面(更占资源)' : 'Original cover (More resource use)';
modelSettingsNote.textContent = zh
? '缩略图由插件自动缓存和回收;视频离开视野或关闭浏览器后会自动释放。'
: 'The plugin automatically caches and evicts thumbnails, and releases videos offscreen or when closed.';
modelSettingsClose.textContent = zh ? '完成' : 'Done';
videoSetting.select.value = this.energySaving ? 'hover' : 'always';
thumbnailSetting.select.value = this.cardThumbnailMode;
};
refreshModelSettingsText();
const closeModelSettings = () => { modelSettingsOverlay.hidden = true; };
modelSettingsBtn.onclick = () => {
refreshModelSettingsText();
settingsHubModal.style.display = 'none';
modelSettingsOverlay.hidden = false;
videoSetting.select.focus();
};
modelSettingsClose.onclick = closeModelSettings;
modelSettingsOverlay.onclick = (event) => {
if (event.target === modelSettingsOverlay) closeModelSettings();
};
videoSetting.select.onchange = () => {
this.energySaving = videoSetting.select.value === 'hover';
localStorage.setItem('anomalous_energy_saving', String(this.energySaving));
this.loadModels();
};
thumbnailSetting.select.onchange = () => {
this.cardThumbnailMode = thumbnailSetting.select.value === 'original' ? 'original' : 'balanced';
localStorage.setItem('anomalous_card_thumbnail_mode', this.cardThumbnailMode);
this.loadModels();
};
const globalScanBtn = document.createElement('button');
globalScanBtn.id = 'anomalous-global-scan-btn';
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🌍 一键全盘极速扫描 (不下载封面/不改名)' : '🌍 Global Quick Scan (No rename/No media)';
styleHubBtn(globalScanBtn);
globalScanBtn.onclick = async () => {
if (!confirm(window.anomalous_browser_lang === 'zh' ? '即将执行极速全盘扫描并同步Hash,此操作不改名也不下载封面,是否继续?' : 'Start global quick scan to sync all hashes?')) return;
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⏳ 扫描中...' : '⏳ Scanning...';
globalScanBtn.disabled = true;
try {
const res = await fetch('/anomalous/scan_all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
use_local_metadata: localStorage.getItem('anomalous_local_metadata_scan') !== 'false',
skip_rename: true,
skip_media: true
})
});
const data = await res.json();
if (data.status === 'ok') {
alert(window.anomalous_browser_lang === 'zh' ? '🚀 全局扫描已后台启动!' : '🚀 Global Scan started in background!');
const pollTimer = setInterval(async () => {
try {
const statusRes = await fetch('/anomalous/global_scan_status');
const statusData = await statusRes.json();
if (!statusData.scanning) {
clearInterval(pollTimer);
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '✅ 扫描完成' : '✅ Scan Complete';
setTimeout(() => {
globalScanBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🌍 一键全盘扫描 (不下载封面/不改名)' : '🌍 Global Quick Scan (No rename/No media)';
globalScanBtn.disabled = false;
}, 3000);
}
} catch (e) { }
}, 3000);
} else {
alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message);
globalScanBtn.disabled = false;
}
} catch (e) {
globalScanBtn.disabled = false;
}
};
const localParseToggleBtn = document.createElement('button');
localParseToggleBtn.id = 'anomalous-local-parse-toggle-btn';
const renderLocalParseToggleBtn = () => {
let isLocalEnabled = localStorage.getItem('anomalous_local_metadata_scan') !== 'false';
localParseToggleBtn.innerHTML = window.anomalous_browser_lang === 'zh'
? (isLocalEnabled ? '☑ 开启非C站模型本地扫描 (读取内置参数) [已开启]' : '☐ 开启非C站模型本地扫描 (读取内置参数) [已关闭]')
: (isLocalEnabled ? '☑ Local metadata scan for non-Civitai [ON]' : '☐ Local metadata scan for non-Civitai [OFF]');
};
renderLocalParseToggleBtn();
styleHubBtn(localParseToggleBtn);
localParseToggleBtn.onclick = () => {
let isLocalEnabled = localStorage.getItem('anomalous_local_metadata_scan') !== 'false';
localStorage.setItem('anomalous_local_metadata_scan', isLocalEnabled ? 'false' : 'true');
renderLocalParseToggleBtn();
};
const checkUnscannedBtn = document.createElement('button');
checkUnscannedBtn.id = 'anomalous-check-unscanned-btn';
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info';
styleHubBtn(checkUnscannedBtn);
checkUnscannedBtn.onclick = async () => {
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⏳ 检查中...' : '⏳ Checking...';
checkUnscannedBtn.disabled = true;
try {
const res = await fetch('/anomalous/all_hashes');
const data = await res.json();
const hashesObj = data.hashes ? data.hashes : data;
let hasUnscanned = false;
for (const key in hashesObj) {
if (hashesObj[key].hash === "") {
hasUnscanned = true;
break;
}
}
if (hasUnscanned) {
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⚠️ 发现缺失,正在自动极速扫描...' : '⚠️ Missing info found, Auto-Scanning...';
const scanRes = await fetch('/anomalous/scan_all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
use_local_metadata: localStorage.getItem('anomalous_local_metadata_scan') !== 'false'
})
});
const scanData = await scanRes.json();
if (scanData.status === 'ok') {
const pollTimer = setInterval(async () => {
try {
const statusRes = await fetch('/anomalous/global_scan_status');
const statusData = await statusRes.json();
if (!statusData.scanning) {
clearInterval(pollTimer);
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '✅ 补全完成' : '✅ Info Complete';
setTimeout(() => {
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info';
checkUnscannedBtn.disabled = false;
}, 3000);
}
} catch (e) { }
}, 3000);
} else {
alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + scanData.message);
checkUnscannedBtn.disabled = false;
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info';
}
} else {
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '✨ 所有模型信息已完整' : '✨ All Model Info is Complete';
setTimeout(() => {
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info';
checkUnscannedBtn.disabled = false;
}, 3000);
}
} catch (e) {
checkUnscannedBtn.disabled = false;
checkUnscannedBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info';
}
};
const scaleContainer = document.createElement('div');
scaleContainer.style.display = 'flex';
scaleContainer.style.alignItems = 'center';
scaleContainer.style.justifyContent = 'space-between';
scaleContainer.style.background = '#1a1a1a';
scaleContainer.style.padding = '8px 12px';
scaleContainer.style.borderRadius = '4px';
scaleContainer.style.border = '2px solid #555';
scaleContainer.style.marginBottom = '4px';
const scaleLabel = document.createElement('span');
scaleLabel.id = 'anomalous-scale-label';
scaleLabel.innerText = window.anomalous_browser_lang === 'zh' ? 'UI 缩放' : 'UI Scale';
scaleLabel.style.color = '#ccc';
scaleLabel.style.fontSize = '0.9em';
let currentScale = parseFloat(savedScale);
const controlsWrapper = document.createElement('div');
controlsWrapper.style.display = 'flex';
controlsWrapper.style.alignItems = 'center';
controlsWrapper.style.gap = '8px';
const scaleVal = document.createElement('span');
scaleVal.innerText = `${Math.round(currentScale * 100)}%`;
scaleVal.style.color = '#fff';
scaleVal.style.fontSize = '0.9em';
scaleVal.style.minWidth = '45px';
scaleVal.style.textAlign = 'center';
const createScaleBtn = (text, delta) => {
const btn = document.createElement('button');
btn.innerText = text;
btn.style.background = '#333';
btn.style.color = '#fff';
btn.style.border = '1px solid #555';
btn.style.borderRadius = '4px';
btn.style.width = '24px';
btn.style.height = '24px';
btn.style.cursor = 'pointer';
btn.style.display = 'flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'center';
btn.onmouseover = () => btn.style.background = '#444';
btn.onmouseout = () => btn.style.background = '#333';
btn.onclick = () => {
currentScale = Math.max(0.5, Math.min(1.5, currentScale + delta));
scaleVal.innerText = `${Math.round(currentScale * 100)}%`;
container.style.setProperty('--anomalous-scale', currentScale);
localStorage.setItem('anomalous_ui_scale', currentScale);
};
return btn;
};
const minusBtn = createScaleBtn('-', -0.1);
const plusBtn = createScaleBtn('+', 0.1);
controlsWrapper.appendChild(minusBtn);
controlsWrapper.appendChild(scaleVal);
controlsWrapper.appendChild(plusBtn);
scaleContainer.appendChild(scaleLabel);
scaleContainer.appendChild(controlsWrapper);
const resetBtn = document.createElement('button');
resetBtn.id = 'anomalous-reset-btn';
resetBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔄 重置界面布局' : '🔄 Reset Layout';
styleHubBtn(resetBtn);
resetBtn.onclick = () => {
if (confirm(window.anomalous_browser_lang === 'zh' ? '确认重置窗口位置、缩放和停靠状态吗?' : 'Reset window position, scale and dock state?')) {
localStorage.removeItem('anomalous_pos_x');
localStorage.removeItem('anomalous_pos_y');
localStorage.removeItem('anomalous_width');
localStorage.removeItem('anomalous_height');
localStorage.removeItem('anomalous_docked');
localStorage.removeItem('anomalous_ui_scale');
container.style.left = '5%';
container.style.top = '5%';
container.style.width = '90%';
container.style.height = '90%';
container.style.setProperty('--anomalous-scale', '1');
currentScale = 1;
scaleVal.innerText = '100%';
if (container.classList.contains('anomalous-docked')) {
container.classList.remove('anomalous-docked');
}
}
};
const hashToggleBtn = document.createElement('button');
hashToggleBtn.id = 'anomalous-hash-toggle-btn';
styleHubBtn(hashToggleBtn);
const updateHashToggleBtn = () => {
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
hashToggleBtn.innerHTML = window.anomalous_browser_lang === 'zh'
? (isInject ? '🟢 注入工作流哈希' : '⚪ 不注入工作流哈希')
: (isInject ? '🟢 Inject Workflow Hash' : '⚪ Skip Workflow Hash');
};
updateHashToggleBtn();
hashToggleBtn.onclick = () => {
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
localStorage.setItem('anomalous_inject_hash', isInject ? 'false' : 'true');
updateHashToggleBtn();
};
const folderManagerBtn = document.createElement('button');
folderManagerBtn.id = 'anomalous-folder-manager-btn';
folderManagerBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '📁 文件夹管理' : '📁 Manage Folders';
styleHubBtn(folderManagerBtn);
folderManagerBtn.onclick = () => {
if (settingsHubModal.style.display !== 'none') {
settingsHubModal.style.display = 'none';
settingsBtn.style.color = '#ccc';
}
this.openFolderManager();
};
// Many redundant buttons have been migrated to the Wizard!
const feedbackBtn = document.createElement('button');
feedbackBtn.id = 'anomalous-feedback-btn';
feedbackBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '💬 提交反馈 / 报告问题' : '💬 Submit Feedback / Report Bug';
styleHubBtn(feedbackBtn);
feedbackBtn.onclick = () => {
window.open('https://github.com/DemonGatanjieu/Anomalous_Model_Browser/issues', '_blank');
if (settingsHubModal.style.display !== 'none') {
settingsHubModal.style.display = 'none';
settingsBtn.style.color = '#ccc';
}
};
settingsHubModal.appendChild(folderManagerBtn);
settingsHubModal.appendChild(modelSettingsBtn);
settingsHubModal.appendChild(scaleContainer);
settingsHubModal.appendChild(langBtn);
settingsHubModal.appendChild(helpBtn);
settingsHubModal.appendChild(feedbackBtn);
settingsHubModal.appendChild(resetBtn);
this.sidebarWrapper.appendChild(settingsHubModal);
const settingsBtn = document.createElement('button');
settingsBtn.id = 'anomalous-global-settings-btn';
settingsBtn.innerHTML = `⚙️`;
settingsBtn.title = window.anomalous_browser_lang === 'zh' ? '设置中心' : 'Settings Hub';
settingsBtn.style.background = 'transparent';
settingsBtn.style.color = '#ccc';
settingsBtn.style.border = 'none';
settingsBtn.style.borderRadius = '6px';
settingsBtn.style.padding = '6px';
settingsBtn.style.fontSize = '1.1em';
settingsBtn.style.marginLeft = 'auto';
settingsBtn.style.cursor = 'pointer';
settingsBtn.style.transition = 'all 0.2s ease';
settingsBtn.onmouseover = () => { settingsBtn.style.background = 'rgba(255,255,255,0.1)'; settingsBtn.style.color = '#fff'; };
settingsBtn.onmouseout = () => { settingsBtn.style.background = 'transparent'; settingsBtn.style.color = '#ccc'; };
const closeSettingsHub = (e) => {
if (settingsHubModal.style.display !== 'none' && !settingsHubModal.contains(e.target) && !settingsBtn.contains(e.target)) {
settingsHubModal.style.display = 'none';
settingsBtn.style.color = '#ccc';
document.removeEventListener('mousedown', closeSettingsHub);
}
};
settingsBtn.onclick = () => {
if (settingsHubModal.style.display === 'none') {
settingsHubModal.style.display = 'flex';
settingsBtn.style.color = '#fff';
// Delay adding the listener slightly to avoid triggering it on the same click
setTimeout(() => document.addEventListener('mousedown', closeSettingsHub), 10);
} else {
settingsHubModal.style.display = 'none';
settingsBtn.style.color = '#ccc';
document.removeEventListener('mousedown', closeSettingsHub);
}
};
const importBtn = document.createElement('button');
importBtn.id = 'anomalous-import-btn';
importBtn.title = t('importBtn');
importBtn.innerHTML = `📥`;
importBtn.style.background = 'transparent';
importBtn.style.color = '#ccc';
importBtn.style.border = 'none';
importBtn.style.borderRadius = '6px';
importBtn.style.padding = '6px';
importBtn.style.fontSize = '1.1em';
importBtn.style.cursor = 'pointer';
importBtn.style.transition = 'all 0.2s ease';
importBtn.onmouseover = () => { importBtn.style.background = 'rgba(255,255,255,0.1)'; importBtn.style.color = '#fff'; };
importBtn.onmouseout = () => { importBtn.style.background = 'transparent'; importBtn.style.color = '#ccc'; };
importBtn.onclick = () => {
if (window.AMB_WorkflowShare) {
window.AMB_WorkflowShare.showUnifiedModal();
} else {
alert(window.anomalous_browser_lang === 'zh' ? '模块尚未加载,请稍等或刷新页面!' : 'Module not loaded, please refresh!');
}
};
const doctorBtn = document.createElement('button');
doctorBtn.id = 'anomalous-doctor-btn';
doctorBtn.title = window.anomalous_browser_lang === 'zh' ? '模型医生' : 'Model Doctor';
doctorBtn.innerHTML = `🩺`;
doctorBtn.style.background = 'transparent';
doctorBtn.style.color = '#ccc';
doctorBtn.style.border = 'none';
doctorBtn.style.borderRadius = '6px';
doctorBtn.style.padding = '6px';
doctorBtn.style.fontSize = '1.1em';
doctorBtn.style.cursor = 'pointer';
doctorBtn.style.transition = 'all 0.2s ease';
doctorBtn.onmouseover = () => { doctorBtn.style.background = 'rgba(255,255,255,0.1)'; doctorBtn.style.color = '#fff'; };
doctorBtn.onmouseout = () => { doctorBtn.style.background = 'transparent'; doctorBtn.style.color = '#ccc'; };
doctorBtn.onclick = async () => {
this.hideAllPanels();
if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') {
container.classList.add('anomalous-sidebar-closed');
} else {
container.classList.remove('anomalous-sidebar-closed');
}
menuBtn.disabled = false;
menuBtn.style.opacity = '1';
menuBtn.style.cursor = 'pointer';
this.doctorPanel.style.display = 'flex';
if (!this.doctorPanelInitialized) {
this.initDoctorPanel();
}
// Trigger auto hash-resolve when opening Doctor
if (window.anomalous_resolve_all_missing_nodes) {
window.anomalous_resolve_all_missing_nodes(true, false).then(() => {
if (app.graph && app.graph.extra && app.graph.extra.anomalous_hashes) {
this.renderGlobalDashboard(app.graph.extra.anomalous_hashes);
}
});
}
};
const assistantBtn = document.createElement('button');
assistantBtn.id = 'anomalous-assistant-btn';
assistantBtn.title = window.anomalous_browser_lang === 'zh' ? '节点助手' : 'Node Assistant';
assistantBtn.innerHTML = `🤖`;
assistantBtn.style.background = 'transparent';
assistantBtn.style.color = '#ccc';
assistantBtn.style.border = 'none';
assistantBtn.style.borderRadius = '6px';
assistantBtn.style.padding = '6px';
assistantBtn.style.fontSize = '1.1em';
assistantBtn.style.cursor = 'pointer';
assistantBtn.style.transition = 'all 0.2s ease';
assistantBtn.onmouseover = () => { assistantBtn.style.background = 'rgba(255,255,255,0.1)'; assistantBtn.style.color = '#fff'; };
assistantBtn.onmouseout = () => { assistantBtn.style.background = 'transparent'; assistantBtn.style.color = '#ccc'; };
assistantBtn.onclick = async () => {
this.hideAllPanels();
if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') {
container.classList.add('anomalous-sidebar-closed');
} else {
container.classList.remove('anomalous-sidebar-closed');
}
menuBtn.disabled = false;
menuBtn.style.opacity = '1';
menuBtn.style.cursor = 'pointer';
this.assistantPanel.style.display = 'flex';
if (!this.assistantPanelInitialized) {
this.initAssistantPanel();
}
// Show current selected node immediately
if (Object.keys(app.canvas.selected_nodes || {}).length > 0) {
const firstSelected = Object.values(app.canvas.selected_nodes)[0];
this.diagnoseNode(firstSelected);
} else {
this.diagnoseNode(null);
}
};
this.sidebarActions.appendChild(doctorBtn);
this.sidebarActions.appendChild(assistantBtn);
this.sidebarActions.appendChild(importBtn);
this.sidebarActions.appendChild(settingsBtn);
this.grid = document.createElement('div');
this.grid.id = 'anomalous-grid';
this.detailPanel = document.createElement('div');
this.detailPanel.id = 'anomalous-detail';
this.detailPanel.style.display = 'none';
this.galleryPanel = document.createElement('div');
this.galleryPanel.id = 'anomalous-gallery-panel';
this.doctorPanel = document.createElement('div');
this.doctorPanel.id = 'anomalous-doctor-panel';
this.doctorPanel.style.display = 'none';
this.doctorPanel.style.flexDirection = 'column';
this.doctorPanel.style.flex = '1';
this.doctorPanel.style.overflowY = 'auto';
this.doctorPanel.style.boxSizing = 'border-box';
this.doctorPanelInitialized = false;
this.assistantPanel = document.createElement('div');
this.assistantPanel.id = 'anomalous-assistant-panel';
this.assistantPanel.style.display = 'none';
this.assistantPanel.style.flexDirection = 'column';
this.assistantPanel.style.flex = '1';
this.assistantPanel.style.overflowY = 'auto';
this.assistantPanel.style.boxSizing = 'border-box';
this.assistantPanelInitialized = false;
this.galleryGrid = document.createElement('div');
this.galleryGrid.className = 'anomalous-gallery-grid';
this.galleryPanel.appendChild(this.galleryGrid);
this.gallerySentinel = document.createElement('div');
this.gallerySentinel.className = 'anomalous-gallery-sentinel';
this.galleryGrid.appendChild(this.gallerySentinel);
this.galleryCurrentPage = 1;
this.galleryLoaded = false;
this.galleryLoading = false;
this.galleryHasMore = true;
this.galleryObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !this.galleryLoading && this.galleryHasMore) {
this.loadGalleryImages(this.galleryCurrentPage + 1);
}
}, { root: this.galleryGrid, rootMargin: '100px' });
this.galleryObserver.observe(this.gallerySentinel);
this.nbPanel = document.createElement('div');
this.nbPanel.className = 'anomalous-nb-modal';
this.nbPanel.style.display = 'none';
this.nbPanel.onclick = (e) => {
if (e.target === this.nbPanel) this.nbPanel.style.display = 'none';
};
content.appendChild(header);
content.appendChild(this.grid);
content.appendChild(this.detailPanel);
content.appendChild(this.galleryPanel);
content.appendChild(this.doctorPanel);
content.appendChild(this.assistantPanel);
container.appendChild(this.sidebarWrapper);
container.appendChild(content);
container.appendChild(this.nbPanel);
this.modal.appendChild(container);
// Resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'anomalous-resize-handle';
let isResizing = false;
resizeHandle.onmousedown = (e) => {
e.preventDefault();
e.stopPropagation();
isResizing = true;
};
window.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const rect = container.getBoundingClientRect();
let newWidth = e.clientX - rect.left;
let newHeight = e.clientY - rect.top;
if (newWidth < 600) newWidth = 600;
if (newHeight < 400) newHeight = 400;
container.style.width = newWidth + 'px';
container.style.height = newHeight + 'px';
});
window.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
localStorage.setItem('anomalous_width', container.style.width);
localStorage.setItem('anomalous_height', container.style.height);
}
});
const savedW = localStorage.getItem('anomalous_width');
const savedH = localStorage.getItem('anomalous_height');
if (savedW) container.style.width = savedW;
if (savedH) container.style.height = savedH;
container.appendChild(resizeHandle);
document.body.appendChild(this.modal);
}
export function renderSidebar() {
this.sidebar.innerHTML = '';
const topBar = document.createElement('div');
topBar.style.display = 'flex';
topBar.style.justifyContent = 'space-between';
topBar.style.alignItems = 'center';
topBar.style.padding = '10px 15px 15px 15px';
const title = document.createElement('h3');
title.innerHTML = t('folders');
title.style.color = '#fff';
title.style.margin = '0';
const isAllCollapsed = this.expandedFolders.size === 0;
const collapseAllBtn = document.createElement('button');
collapseAllBtn.innerHTML = isAllCollapsed ? (window.anomalous_browser_lang === 'zh' ? '➕ 展开全部' : '➕ Expand All') : (window.anomalous_browser_lang === 'zh' ? '➖ 收起全部' : '➖ Collapse All');
collapseAllBtn.style.padding = '4px 8px';
collapseAllBtn.style.background = '#444';
collapseAllBtn.style.color = '#fff';
collapseAllBtn.style.border = 'none';
collapseAllBtn.style.borderRadius = '4px';
collapseAllBtn.style.cursor = 'pointer';
collapseAllBtn.onclick = () => {
if (isAllCollapsed) {
(this.foldersData || []).forEach(typeGroup => {
this.expandedFolders.add(typeGroup.type);
Object.keys(typeGroup.folders).forEach(path => {
this.expandedFolders.add(typeGroup.type + path);
});
});
} else {
this.expandedFolders.clear();
}
this.renderSidebar();
};
topBar.appendChild(title);
topBar.appendChild(collapseAllBtn);
this.sidebar.appendChild(topBar);
const searchBox = document.createElement('div');
searchBox.style.padding = '0 15px 15px 15px';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = window.anomalous_browser_lang === 'zh' ? '🔍 搜索模型...' : '🔍 Search models...';
searchInput.style.width = '100%';
searchInput.style.padding = '8px 12px';
searchInput.style.borderRadius = '8px';
searchInput.style.border = '1px solid rgba(255,255,255,0.1)';
searchInput.style.background = 'rgba(0,0,0,0.2)';
searchInput.style.color = '#fff';
searchInput.style.boxSizing = 'border-box';
searchInput.style.outline = 'none';
searchInput.style.transition = 'border-color 0.2s';
searchInput.onfocus = () => searchInput.style.border = '1px solid #007aff';
searchInput.onblur = () => searchInput.style.border = '1px solid rgba(255,255,255,0.1)';
searchInput.oninput = (e) => {
if (this.currentDetailModel) {
this.detailPanel.style.display = 'none';
this.stopMediaInContainer(this.detailPanel);
this.detailPanel.innerHTML = '';
this.currentDetailModel = null;
this.grid.style.display = 'grid';
}
const val = e.target.value.toLowerCase();
const cards = this.grid.querySelectorAll('.anomalous-card');
cards.forEach(card => {
const titleEl = card.querySelector('.anomalous-card-title');
if (!titleEl) return;
const titleText = titleEl.innerText.toLowerCase();
if (titleText.includes(val)) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
};
searchBox.appendChild(searchInput);
this.sidebar.appendChild(searchBox);
(this.foldersData || []).forEach(typeGroup => {
const header = document.createElement('div');
header.className = 'anomalous-type-header';
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.cursor = 'pointer';
const isTypeExpanded = this.expandedFolders.has(typeGroup.type);
header.innerHTML = `<span>${escapeHtml(typeGroup.label)}</span> <span>${isTypeExpanded ? '▼' : '▶'}</span>`;
header.onclick = () => {
if (isTypeExpanded) this.expandedFolders.delete(typeGroup.type);
else this.expandedFolders.add(typeGroup.type);
this.renderSidebar();
};
this.sidebar.appendChild(header);
if (!isTypeExpanded) return;
const sortedPaths = Object.keys(typeGroup.folders).sort();
sortedPaths.forEach(path => {
const info = typeGroup.folders[path];
const parts = path.split('/').filter(p => p);
const parentPath = parts.length > 1 ? '/' + parts.slice(0, -1).join('/') : '/';
if (path !== '/' && parentPath !== '/') {
let parentId = typeGroup.type + parentPath;
if (!this.expandedFolders.has(parentId)) return;
}
const hasChildren = sortedPaths.some(p => p !== path && p.startsWith(path === '/' ? '/' : path + '/'));
const item = document.createElement('div');
item.className = 'anomalous-folder-item';
const depth = path === '/' ? 0 : parts.length;
item.style.paddingLeft = (15 + depth * 15) + 'px';
const myId = typeGroup.type + path;
const isExpanded = this.expandedFolders.has(myId);
let toggleIcon = '';
if (hasChildren) {
toggleIcon = `<span class="anomalous-folder-toggle" style="margin-right: 8px; width: 12px; display: inline-block; font-size: 0.8em; color: #888;">${isExpanded ? '▼' : '▶'}</span>`;
} else {
toggleIcon = `<span style="margin-right: 8px; width: 12px; display: inline-block;"></span>`;
}
item.innerHTML = `${toggleIcon}<span class="anomalous-folder-name" style="color: #ddd;">${escapeHtml(info.name)}</span> <span style="opacity:0.4; font-size:0.8em; margin-left: 5px;">${escapeHtml(info.model_count)}</span>`;
if (this.currentType === typeGroup.type && this.currentPathIdx === typeGroup.path_idx && this.currentSubfolder === path) {
item.classList.add('active');
}
item.onclick = (e) => {
if (e.target.classList.contains('anomalous-folder-toggle')) {
if (isExpanded) this.expandedFolders.delete(myId);
else this.expandedFolders.add(myId);
this.renderSidebar();
return;
}
this.currentType = typeGroup.type;
this.currentPathIdx = typeGroup.path_idx;
this.currentSubfolder = path;
this.hideAllPanels();
this.grid.style.display = 'grid';
this.renderSidebar();
this.loadModels();
};
this.sidebar.appendChild(item);
});
});
}
export async function loadFolders() {
try {
const res = await fetch('/anomalous/folders');
const data = await res.json();
this.foldersData = data.folders || [];
if (!this.firstLoadDone && this.foldersData.length > 0) {
this.firstLoadDone = true;
let found = false;
for (const typeGroup of this.foldersData) {
const sortedPaths = Object.keys(typeGroup.folders).sort();
for (const path of sortedPaths) {
if (typeGroup.folders[path].model_count > 0) {
this.currentType = typeGroup.type;
this.currentPathIdx = typeGroup.path_idx;
this.currentSubfolder = path;
found = true;
break;
}
}
if (found) break;
}
}
// Auto expand all
(this.foldersData || []).forEach(typeGroup => {
this.expandedFolders.add(typeGroup.type);
Object.keys(typeGroup.folders).forEach(path => {
this.expandedFolders.add(typeGroup.type + path);
});
});
this.renderSidebar();
this.loadModels();
} catch (e) { }
}
export function showHelp() {
if (this.helpModal) {
this.helpModal.remove();
}
this.helpModal = document.createElement('div');
this.helpModal.style.position = 'absolute';
this.helpModal.style.top = '0';
this.helpModal.style.left = '0';
this.helpModal.style.width = '100%';
this.helpModal.style.height = '100%';
this.helpModal.style.background = 'rgba(0,0,0,0.85)';
this.helpModal.style.zIndex = '9999';
this.helpModal.style.display = 'flex';
this.helpModal.style.alignItems = 'center';
this.helpModal.style.justifyContent = 'center';
const box = document.createElement('div');
box.style.background = 'var(--bg-color, #222)';
box.style.border = '1px solid var(--border-color, #444)';
box.style.borderRadius = '8px';
box.style.width = '550px';
box.style.maxWidth = '90%';
box.style.boxShadow = '0 10px 40px rgba(0,0,0,0.8)';
box.style.display = 'flex';
box.style.flexDirection = 'column';
box.style.maxHeight = '90vh';
const header = document.createElement('div');
header.style.padding = '15px 20px';
header.style.borderBottom = '1px solid #444';
header.style.background = '#333';
header.style.display = 'flex';
header.style.alignItems = 'center';
header.style.justifyContent = 'space-between';
const title = document.createElement('h2');
title.innerHTML = t('helpTitle');
title.style.margin = '0';
title.style.color = '#fff';
title.style.fontSize = '1.2em';
const closeX = document.createElement('div');
closeX.innerHTML = '&times;';
closeX.style.position = 'absolute';
closeX.style.top = '10px';
closeX.style.right = '15px';
closeX.style.fontSize = '1.8em';
closeX.style.cursor = 'pointer';
closeX.style.color = '#ff4444';
closeX.onclick = () => this.helpModal.remove();
header.appendChild(title);
header.appendChild(closeX);
const body = document.createElement('div');
body.style.padding = '20px';
body.innerHTML = t('helpContent');
body.style.overflowY = 'auto';
body.style.flex = '1';
const footer = document.createElement('div');
footer.style.padding = '15px';
footer.style.borderTop = '1px solid #444';
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
const closeBtn = document.createElement('button');
closeBtn.innerHTML = t('closeHelp');
closeBtn.style.padding = '8px 16px';
closeBtn.style.background = '#444';
closeBtn.style.color = '#fff';
closeBtn.style.border = 'none';
closeBtn.style.borderRadius = '4px';
closeBtn.style.cursor = 'pointer';
closeBtn.onclick = () => this.helpModal.remove();
footer.appendChild(closeBtn);
box.appendChild(header);
box.appendChild(body);
box.appendChild(footer);
this.helpModal.appendChild(box);
document.getElementById('anomalous-container').appendChild(this.helpModal);
}
export function hideAllPanels() {
this.grid.style.display = 'none';
this.detailPanel.style.display = 'none';
if (this.galleryPanel) this.galleryPanel.style.display = 'none';
if (this.nbPanel) this.nbPanel.style.display = 'none';
if (this.doctorPanel) this.doctorPanel.style.display = 'none';
if (this.assistantPanel) this.assistantPanel.style.display = 'none';
if (this.currentDetailObserver) {
this.currentDetailObserver.disconnect();
this.currentDetailObserver = null;
}
}
export async function openFolderManager() {
let modal = document.getElementById('anomalous-folder-manager-modal');
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = 'anomalous-folder-manager-modal';
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, "Segoe UI", sans-serif';
const content = document.createElement('div');
content.style.background = '#1e1e1e';
content.style.borderRadius = '12px';
content.style.padding = '24px';
content.style.width = '500px';
content.style.maxWidth = '90%';
content.style.maxHeight = '85vh';
content.style.display = 'flex';
content.style.flexDirection = 'column';
content.style.boxShadow = '0 10px 30px rgba(0,0,0,0.5)';
const header = document.createElement('h2');
header.innerText = window.anomalous_browser_lang === 'zh' ? '📁 侧边栏文件夹管理' : '📁 Manage Folders';
header.style.margin = '0 0 16px 0';
header.style.color = '#fff';
header.style.fontSize = '1.4em';
content.appendChild(header);
const desc = document.createElement('div');
desc.innerText = window.anomalous_browser_lang === 'zh'
? '上下拖拽调整侧边栏的显示顺序。点击眼睛图标可隐藏不需要显示的底层文件夹。隐藏的文件夹不会被扫描,完全不占用性能。'
: 'Drag and drop to reorder folders. Click the eye icon to hide unneeded folders. Hidden folders are skipped during scanning and consume zero performance.';
desc.style.color = '#aaa';
desc.style.fontSize = '0.9em';
desc.style.marginBottom = '20px';
desc.style.lineHeight = '1.5';
content.appendChild(desc);
const toggleContainer = document.createElement('div');
toggleContainer.style.display = 'flex';
toggleContainer.style.alignItems = 'center';
toggleContainer.style.justifyContent = 'center';
toggleContainer.style.marginBottom = '15px';
toggleContainer.style.gap = '20px';
toggleContainer.style.background = '#222';
toggleContainer.style.padding = '10px';
toggleContainer.style.borderRadius = '8px';
toggleContainer.style.border = '1px solid #444';
const abstractRadio = document.createElement('input');
abstractRadio.type = 'radio';
abstractRadio.name = 'viewMode';
abstractRadio.value = 'abstract';
abstractRadio.id = 'anomalous_mode_abstract';
const abstractLabel = document.createElement('label');
abstractLabel.htmlFor = 'anomalous_mode_abstract';
abstractLabel.innerText = window.anomalous_browser_lang === 'zh' ? '分类模式 (ComfyUI)' : 'Category Mode (ComfyUI)';
abstractLabel.style.cursor = 'pointer';
abstractLabel.style.color = '#ccc';
const physicalRadio = document.createElement('input');
physicalRadio.type = 'radio';
physicalRadio.name = 'viewMode';
physicalRadio.value = 'physical';
physicalRadio.id = 'anomalous_mode_physical';
const physicalLabel = document.createElement('label');
physicalLabel.htmlFor = 'anomalous_mode_physical';
physicalLabel.innerText = window.anomalous_browser_lang === 'zh' ? '物理模式 (本地文件夹)' : 'Physical Mode (Local)';
physicalLabel.style.cursor = 'pointer';
physicalLabel.style.color = '#ccc';
const div1 = document.createElement('div');
div1.style.display = 'flex';
div1.style.alignItems = 'center';
div1.style.gap = '6px';
div1.appendChild(abstractRadio);
div1.appendChild(abstractLabel);
const div2 = document.createElement('div');
div2.style.display = 'flex';
div2.style.alignItems = 'center';
div2.style.gap = '6px';
div2.appendChild(physicalRadio);
div2.appendChild(physicalLabel);
toggleContainer.appendChild(div1);
toggleContainer.appendChild(div2);
content.appendChild(toggleContainer);
const listContainer = document.createElement('div');
listContainer.style.flex = '1';
listContainer.style.overflowY = 'auto';
listContainer.style.border = '1px solid #444';
listContainer.style.borderRadius = '8px';
listContainer.style.background = '#2a2a2a';
listContainer.style.padding = '8px';
content.appendChild(listContainer);
let typesData = [];
let currentMode = 'abstract';
const fetchData = async () => {
try {
const res = await fetch('/anomalous/all_folder_types');
const data = await res.json();
typesData = data.folder_types || [];
currentMode = data.folder_view_mode || 'abstract';
if (currentMode === 'physical') {
physicalRadio.checked = true;
} else {
abstractRadio.checked = true;
}
typesData.sort((a, b) => {
if (a.visible && !b.visible) return -1;
if (!a.visible && b.visible) return 1;
return 0;
});
renderList();
} catch(e) {
alert("Failed to load folder types.");
}
};
const onModeSwitch = async (e) => {
const newMode = e.target.value;
if (newMode === currentMode) return;
try {
await fetch('/anomalous/save_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folder_view_mode: newMode })
});
await fetchData();
this.firstLoadDone = false;
this.expandedFolders.clear();
await this.loadFolders();
} catch(err) {
alert("Error switching mode");
}
};
abstractRadio.addEventListener('change', onModeSwitch);
physicalRadio.addEventListener('change', onModeSwitch);
let dragSrcEl = null;
const renderList = () => {
listContainer.innerHTML = '';
typesData.forEach((item, index) => {
const row = document.createElement('div');
row.className = 'anomalous-folder-manager-row';
row.draggable = true;
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '10px 12px';
row.style.margin = '4px 0';
row.style.background = '#333';
row.style.borderRadius = '6px';
row.style.cursor = 'grab';
row.style.border = '1px solid transparent';
row.dataset.index = index;
row.dataset.type = item.type;
row.dataset.visible = item.visible;
row.addEventListener('dragstart', function(e) {
this.style.opacity = '0.4';
dragSrcEl = this;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
});
row.addEventListener('dragover', function(e) {
if (e.preventDefault) e.preventDefault();
e.dataTransfer.dropEffect = 'move';
return false;
});
row.addEventListener('dragenter', function(e) {
this.style.border = '1px dashed #8AB4F8';
});
row.addEventListener('dragleave', function(e) {
this.style.border = '1px solid transparent';
});
row.addEventListener('drop', function(e) {
if (e.stopPropagation) e.stopPropagation();
if (dragSrcEl !== this) {
const fromIdx = parseInt(dragSrcEl.dataset.index);
const toIdx = parseInt(this.dataset.index);
const movedItem = typesData.splice(fromIdx, 1)[0];
typesData.splice(toIdx, 0, movedItem);
renderList();
}
return false;
});
row.addEventListener('dragend', function(e) {
this.style.opacity = '1';
const rows = listContainer.querySelectorAll('.anomalous-folder-manager-row');
rows.forEach(r => r.style.border = '1px solid transparent');
});
const leftGroup = document.createElement('div');
leftGroup.style.display = 'flex';
leftGroup.style.alignItems = 'center';
leftGroup.style.gap = '12px';
const handle = document.createElement('div');
handle.innerHTML = '☰';
handle.style.color = '#888';
handle.style.cursor = 'grab';
const name = document.createElement('div');
name.innerText = item.type;
name.style.color = item.visible ? '#fff' : '#666';
name.style.fontWeight = '500';
leftGroup.appendChild(handle);
leftGroup.appendChild(name);
const visBtn = document.createElement('button');
visBtn.innerHTML = item.visible ? '👁️' : '❌';
visBtn.style.background = 'transparent';
visBtn.style.border = 'none';
visBtn.style.cursor = 'pointer';
visBtn.style.fontSize = '1.2em';
visBtn.style.opacity = item.visible ? '1' : '0.5';
visBtn.title = item.visible ? 'Visible' : 'Hidden';
visBtn.onclick = (e) => {
e.stopPropagation();
typesData[index].visible = !typesData[index].visible;
renderList();
};
row.appendChild(leftGroup);
row.appendChild(visBtn);
listContainer.appendChild(row);
});
};
fetchData(); // initial load
// --- End Drag & Drop Logic ---
const footer = document.createElement('div');
footer.style.display = 'flex';
footer.style.justifyContent = 'flex-end';
footer.style.gap = '12px';
footer.style.marginTop = '20px';
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 = '#ccc';
cancelBtn.style.border = '1px solid #555';
cancelBtn.style.borderRadius = '6px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.onclick = () => modal.remove();
const saveBtn = document.createElement('button');
saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存并刷新' : 'Save & Reload';
saveBtn.style.padding = '8px 16px';
saveBtn.style.background = '#8AB4F8';
saveBtn.style.color = '#1e1e1e';
saveBtn.style.border = 'none';
saveBtn.style.borderRadius = '6px';
saveBtn.style.cursor = 'pointer';
saveBtn.style.fontWeight = 'bold';
saveBtn.onclick = async () => {
saveBtn.innerText = '⏳...';
saveBtn.disabled = true;
try {
const payload = {};
if (currentMode === 'physical') {
payload.physical_folders_config = typesData;
} else {
payload.folder_types_config = typesData;
}
await fetch('/anomalous/save_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
modal.remove();
this.firstLoadDone = false;
this.expandedFolders.clear();
await this.loadFolders();
} catch(e) {
alert("Error saving config: " + e);
saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存并刷新' : 'Save & Reload';
saveBtn.disabled = false;
}
};
footer.appendChild(cancelBtn);
footer.appendChild(saveBtn);
content.appendChild(footer);
modal.appendChild(content);
document.body.appendChild(modal);
}