/** * 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 = `🏠 ${t('models')}`; 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 = `🖼️ ${t('gallery') || '图库'}`; 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 = `❓ ${t('help')}`; helpBtn.onclick = () => this.showHelp(); const nbBtn = document.createElement('button'); nbBtn.id = 'anomalous-notebook-btn'; nbBtn.title = t('notebookTitle'); nbBtn.innerHTML = `📑 ${t('notebooks')}`; 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 = `${t('apiKeyConfig')}`; 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 = `✨ ${window.anomalous_browser_lang === 'zh' ? '虚拟重命名 (安全推荐)' : 'Virtual Rename (Safe)'}`; 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 = `💾 ${window.anomalous_browser_lang === 'zh' ? '物理重命名' : 'Physical Rename'}`; 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' ? '真实修改底层的 .safetensors 及其所有附属文件名,彻底告别乱码文件名。' : 'Permanently rename the underlying .safetensors and associated files on disk.'; pContainer.appendChild(pRow); pContainer.appendChild(pDesc); dualChannelRow.appendChild(vContainer); dualChannelRow.appendChild(pContainer); s2List.appendChild(createListRow('📝', '自动规范命名', 'Auto-Normalize Naming', '强迫症福音。自动将杂乱无章的模型文件名重命名为 模型名_版本名.safetensors 的标准格式。
例如: model_final(1).safetensorsBeautiful_Mix_V1.0.safetensors', 'Automatically rename messy model files to the official standard format: ModelName_VersionName.safetensors.
e.g. model_final(1).safetensorsBeautiful_Mix_V1.0.safetensors', 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 = '×'; 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 = `🏠 ${t('models')}`; galleryBtn.innerHTML = `🖼️ ${t('gallery')}`; scanBtn.title = t('scanTitle'); scanBtn.innerHTML = `🔄`; helpBtn.innerHTML = `❓ ${t('help')}`; nbBtn.title = t('notebookTitle'); nbBtn.innerHTML = `📑 ${t('notebooks')}`; 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; } } const impOverlay = document.getElementById('anomalous-import-overlay'); if (impOverlay && impOverlay.parentNode) { impOverlay.parentNode.removeChild(impOverlay); } apiKeyBtn.innerHTML = `${t('apiKeyConfig')}`; 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 = `${escapeHtml(typeGroup.label)} ${isTypeExpanded ? '▼' : '▶'}`; 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 = `${isExpanded ? '▼' : '▶'}`; } else { toggleIcon = ``; } item.innerHTML = `${toggleIcon}${escapeHtml(info.name)} ${escapeHtml(info.model_count)}`; 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 = '×'; 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); }