| |
| |
| |
| |
|
|
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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; |
| } |
|
|
| |
| 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'; |
|
|
| |
|
|
| 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('notebookTitle'); |
| nbBtn.innerHTML = `📑 <span class="anomalous-btn-text">${t('notebooks')}</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'; |
|
|
| |
| 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'; |
|
|
| |
| 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'; |
|
|
| |
| 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); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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; |
| }; |
|
|
| |
| 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); |
|
|
| |
| 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(); |
|
|
| |
| 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'; |
| } |
| |
| |
| 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') { |
| |
| 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; |
| } |
|
|
| |
| 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'; |
| } |
|
|
| |
| 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; |
| } 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'; |
| 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 = `🏠 <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('notebookTitle'); |
| nbBtn.innerHTML = `📑 <span class="anomalous-btn-text">${t('notebooks')}</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'; |
|
|
| |
| 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 = `<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(); |
| }; |
|
|
| |
| |
| 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'; |
| |
| 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(); |
| } |
| |
| 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(); |
| } |
| |
| 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); |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| (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(); |
| |
| |
|
|
| 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); |
| } |
|
|