From 14541bdfc9bc12f38b1c5753cb4ced9e526f10be Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Sun, 12 Jul 2026 22:11:01 +0800 Subject: [PATCH] Delete web/main.js.rebuilt --- web/main.js.rebuilt | 4373 ------------------------------------------- 1 file changed, 4373 deletions(-) delete mode 100644 web/main.js.rebuilt diff --git a/web/main.js.rebuilt b/web/main.js.rebuilt deleted file mode 100644 index adda0fd..0000000 --- a/web/main.js.rebuilt +++ /dev/null @@ -1,4373 +0,0 @@ -import { app } from "../../scripts/app.js"; - -const i18n = { - zh: { - title: '📦 Anomalous 模型浏览器', - scan: '🔄 扫描目录并同步 C 站数据', - scanTitle: '扫描目录', - scanConfirm: '注意:\n扫描过程将会自动比对 Civitai 并重命名本地模型,同时清理损坏文件。\n确定要开始扫描吗?', - scanning: '扫描中...', - scanBg: '🚀 扫描后台启动!', - scanDone: '完成', - scanCompleteMsg: '✅ 扫描完成!\n\n⚠️ 提示:由于部分模型已被重命名或去重,请点击 ComfyUI 的 [Refresh] 按钮以同步最新的模型列表。', - eco: '🔋 视频悬浮播放 (节能模式)', - autoPlay: '🎬 自动播放视频封面 (高能耗)', - togglePlayTitle: '切换播放模式', - clean: '🧹 清理无效的 .info 残留文件', - cleanTitle: '清理重复 Info', - cleanConfirm: '将扫描并删除所有无用的 .civitai.info 残留文件。是否继续?', - cleaning: '清理中', - cleanDone: '✅ 清理完毕!删除了', - files: '个文件。', - cleanFail: '❌ 清理失败: ', - cleanErr: '❌ 清理出错: ', - folders: '📂 文件夹', - collapseAll: '➖ 收起全部', - expandAll: '➕ 展开全部', - noModels: '此文件夹中没有找到模型。', - noPreview: '暂无预览图', - clickScan: '点击扫描', - back: '⬅️ 返回网格', - delModel: '🗑️ 删除', - delConfirm: '确定彻底删除', - delConfirm2: '及其所有关联配置、预览图和缓存文件吗?此操作不可逆!', - delSuccess: '✅ 删除成功!\n', - delNote: '\n\n⚠️ 提示:请重启 ComfyUI,否则由于缓存继续使用会报错', - delFail: '❌ 删除失败: ', - copyAll: '📋 复制全部', - copied: '✅ 已复制!', - clickToCopy: '点击复制: ', - compatibleModels: '🔗 兼容模型', - loadingCompatible: '加载中...', - backToPrev: '🔙 返回上一层', - applyToCanvas: '➕ 插入节点', - applySuccess: '✅ 已添加至工作流', - help: '帮助', - helpTitle: '📖 使用说明', - helpContent: ` -
-

1. 🔑 API Key (Civitai): 在顶部输入你的 API Key,可用于扫描时获取限制级 (NSFW) 模型的封面和信息。

-

2. 🔄 扫描 (Scan): 自动扫描当前选中的文件夹,从 C 站匹配并下载封面图,同时规范化重命名你的本地模型文件。

-

3. 🧹 清理 (Clean Info): 全局深度清理:自动扫描并删除所有因为模型被删而遗留的无效 .civitai.info 文件。

-

4. 🔋 节能 / 🎬 自动播放: 切换视频封面的播放行为。“节能”模式下,鼠标悬浮在卡片上时才会播放预览视频。

-

5. 🗂️ 详情与删除: 点击任何模型卡片,可查看介绍并一键复制触发词。也可以在此处彻底删除模型及关联配置(删除后须重启 ComfyUI 引擎生效)。

-

6. 📂 目录折叠: 点击左侧边栏的“收起/展开全部”快速管理网格视图。

-

7. 🔗 兼容模型匹配: 点开大模型或 Lora 详细页,系统将自动基于 Base Model 架构,双向匹配并展示关联模型。

-

8. ➕ 一键投放节点: 在网格卡片右上角或详细页顶部,点击【➕】按钮,可将当前模型节点直接贴在鼠标上,并无缝插入工作流画布!

-

9. 📑 笔记本 (Notebook): 提供强大的工作流草稿本功能。你可以创建不同的笔记本,选择基础架构 (Base Model),系统会自动为你筛选出对应的纯净主模型和 Lora 画廊供你点选。同时内置了极具高级感的双语提示词对照编辑器,支持标签化悬停高亮与快捷复制,并且支持将整个配置一键发送到画布!

-

10. 🖼️ 图库 (Gallery): 同步浏览 ComfyUI output 文件夹的所有生成历史。支持无限懒加载,点击全屏预览,设有极度安全的防误触删除遮罩。更可以直接将任意图片拖拽到画布上瞬间还原内嵌的工作流!

-

11. 🪄 智能修复飘红节点 (Auto-Fix): 设置面板中的“一键修复工作流飘红”功能或缺模型弹窗中的一键按钮,可以帮您瞬间自动替换画布上所有的报错模型。⚠️ 必须知晓:系统依赖模型哈希(Hash)进行精准匹配,因此只有在使用本插件之后生成/保存的工作流或图片,才能实现 100% 精准全自动修复。对于历史遗留的旧工作流,由于缺失哈希信息,无法自动修复,敬请手动重选。

-
`, - notebooks: '笔记本', - notebookTitle: '笔记本管理', - createNotebook: '新建', - saveNotebook: '💾 保存', - deleteNotebook: '🗑️ 删除', - renameNotebook: '✏️ 重命名', - baseModel: 'Base Model (基础模型)', - mainModel: 'Main Model (主模型)', - sendToCanvas: '🚀 发送到画布', - translate: '翻译', - newNotebookName: '新笔记本名称', - noBaseModel: '请先选择 Base Model', - closeHelp: '关闭说明', - delSure: '⚠️ 确定吗?', - dockTitle: '侧边停靠', - replaceAll: '全部替换', - editRaw: '📝 纯文本/粘贴', - findPlaceholder: '查找...', - replacePlaceholder: '替换为...', - models: '模型', - gallery: '图库', - apiKeyConfig: '🔑 Civitai API 密钥配置 (用于下载封面)', - apiKeyPrompt: '请输入你的 Civitai API Key:', - settingsBtn: '设置', - closeSettings: '✖ 关闭设置面板', - }, - en: { - title: '📦 Anomalous Model Browser', - scan: '🔄 Scan folder and sync Civitai data', - scanTitle: 'Scan Folder', - scanConfirm: 'Notice:\nThe scan process will automatically compare with Civitai and rename your local files.\nStart scan?', - scanning: 'Scanning...', - scanBg: '🚀 Scan started in background!', - scanDone: 'Done', - scanCompleteMsg: '✅ Scan Complete!\n\n⚠️ Note: Please click [Refresh] to sync the model list since files were renamed.', - eco: '🔋 Eco Mode (Play video on hover)', - autoPlay: '🎬 AutoPlay Videos (High perf cost)', - togglePlayTitle: 'Toggle Play Mode', - clean: '🧹 Clean orphaned .info files', - cleanTitle: 'Clean Duplicate Info', - cleanConfirm: 'This will globally scan and delete all redundant .civitai.info files. Continue?', - cleaning: 'Cleaning', - cleanDone: '✅ Clean Complete! Deleted', - files: 'files.', - cleanFail: '❌ Failed: ', - cleanErr: '❌ Error: ', - folders: '📂 Folders', - collapseAll: '➖ Collapse All', - expandAll: '➕ Expand All', - noModels: 'No models found in this folder.', - noPreview: 'No preview available', - clickScan: 'Click Scan', - back: '⬅️ Back to grid', - delModel: '🗑️ Delete', - delConfirm: 'Are you sure you want to permanently delete', - delConfirm2: ' and all its associated configs, previews, and cache files? This action is irreversible!', - delSuccess: '✅ Delete Complete!\n', - delNote: '\n\n⚠️ Note: Please restart ComfyUI, otherwise continuing to use it due to cache will cause errors.', - delFail: '❌ Delete Failed: ', - copyAll: '📋 Copy All', - copied: '✅ Copied!', - clickToCopy: 'Click to copy: ', - compatibleModels: '🔗 Compatible Models', - loadingCompatible: 'Loading...', - backToPrev: '🔙 Back to prev', - applyToCanvas: '➕ Add Node', - applySuccess: '✅ Added to workflow', - help: 'Help', - helpTitle: '📖 User Manual', - helpContent: ` -
-

1. 🔑 API Key (Civitai): Enter your key at the top to fetch metadata and previews for NSFW models during scanning.

-

2. 🔄 Scan: Automatically scans the current folder, fetches Civitai metadata, downloads previews, and standardizes model filenames.

-

3. 🧹 Clean Info: Globally scans and deletes orphaned .civitai.info files to free up disk space.

-

4. 🔋 Eco / 🎬 AutoPlay: Toggle video playback modes. Eco mode plays videos only when hovering over a model card.

-

5. 🗂️ Details & Delete: Click a model card to view details and 1-click copy trained words. You can also permanently delete the model here (requires ComfyUI restart).

-

6. 📂 Folder Toggle: Use the Collapse/Expand All button in the sidebar to manage your view.

-

7. 🔗 Compatible Model Matching: Open a Checkpoint or Lora detail page, and the system will automatically bi-directionally match and display compatible models based on the Base Model architecture.

-

8. ➕ One-Click Add Node: Click the 【➕】 button on a grid card or detail page to instantly attach the model node to your cursor and drop it seamlessly onto your workflow canvas!

-

9. 📑 Notebook System: A powerful drafting workspace. Create notebooks, select a Base Model, and precisely match compatible Main Models and Loras. Includes a premium dual-pane bilingual prompt editor with hover-sync, tag splitting, and 1-click deployment to canvas.

-

10. 🖼️ Gallery System: Natively browse your ComfyUI output history. Features infinite lazy loading, fullscreen immersive previews, and a foolproof overlay for safe deletion. You can even drag and drop any image directly onto your canvas to instantly import its workflow!

-

11. 🪄 Auto-Fix Missing Models: The "Fix Models" button in Settings or the One-Click button in the missing-models popup will instantly repair all red/missing model nodes on your canvas. ⚠️ Important: The system relies on model hashes for 100% accurate matching. Therefore, ONLY workflows or images generated/saved AFTER installing this plugin can be fully auto-repaired. For older historical workflows lacking hash data, auto-fix is not possible and you must manually re-select the models.

-
`, - notebooks: 'Notebooks', - notebookTitle: 'Notebook Manager', - createNotebook: 'New', - saveNotebook: '💾 Save', - deleteNotebook: '🗑️ Delete', - renameNotebook: '✏️ Rename', - baseModel: 'Base Model', - mainModel: 'Main Model', - sendToCanvas: '🚀 Send to Canvas', - translate: 'Translate', - newNotebookName: 'New Notebook Name', - noBaseModel: 'Select a Base Model first', - closeHelp: 'Close Manual', - delSure: '⚠️ Sure?', - dockTitle: 'Dock to Left', - replaceAll: 'Replace All', - editRaw: '📝 Edit Raw / Paste', - findPlaceholder: 'Find...', - replacePlaceholder: 'Replace with...', - models: 'Models', - gallery: 'Gallery', - apiKeyConfig: '🔑 Civitai API Key Config (For downloading previews)', - apiKeyPrompt: 'Enter your Civitai API Key:', - settingsBtn: 'Settings', - closeSettings: '✖ Close Settings', - } -}; - -let defaultLang = 'zh'; -try { - let comfyDetected = false; - const aglLang = localStorage.getItem('Comfy.Settings.AIGODLIKE-COMFYUI-TRANSLATION.Language'); - if (aglLang) { - defaultLang = aglLang.toLowerCase().includes('en') ? 'en' : 'zh'; - comfyDetected = true; - } else { - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && (key.toLowerCase().includes('lang') || key.toLowerCase().includes('locale'))) { - const val = localStorage.getItem(key); - if (typeof val === 'string') { - const lowerVal = val.toLowerCase(); - if (lowerVal.includes('zh') || lowerVal.includes('chinese')) { - defaultLang = 'zh'; - comfyDetected = true; - break; - } else if (lowerVal.includes('en') || lowerVal.includes('english')) { - defaultLang = 'en'; - comfyDetected = true; - break; - } - } - } - } - } - - if (!comfyDetected && navigator.language && !navigator.language.toLowerCase().startsWith('zh')) { - defaultLang = 'en'; - } -} catch (e) { - if (navigator.language && !navigator.language.toLowerCase().startsWith('zh')) { - defaultLang = 'en'; - } -} -let currentLang = localStorage.getItem('anomalous_lang') || defaultLang; -window.anomalous_browser_lang = currentLang; -const t = (key) => i18n[currentLang][key] || key; - -class AnomalousBrowser { - constructor() { - this.modal = null; - this.sidebar = null; - this.grid = null; - this.detailPanel = null; - this.currentType = 'loras'; - this.currentPathIdx = 0; - this.currentSubfolder = '/'; - this.foldersData = null; - this.expandedFolders = new Set(['/', 'checkpoints', 'loras', 'unet', 'diffusion_models']); - this.energySaving = localStorage.getItem('anomalous_energy_saving') === 'true'; - this.createDOM(); - } - - createDOM() { - this.modal = document.createElement('div'); - this.modal.id = 'anomalous-modal'; - - const container = document.createElement('div'); - container.id = 'anomalous-container'; - - const updateLangClass = () => { - if (currentLang === 'en') container.classList.add('anomalous-lang-en'); - else container.classList.remove('anomalous-lang-en'); - }; - updateLangClass(); - - const savedScale = localStorage.getItem('anomalous_ui_scale') || 1; - container.style.setProperty('--anomalous-scale', savedScale); - - // Sidebar - this.sidebarWrapper = document.createElement('div'); - this.sidebarWrapper.id = 'anomalous-sidebar-wrapper'; - this.sidebarWrapper.style.position = 'relative'; - - const brandBar = document.createElement('div'); - brandBar.style.padding = '15px 15px 10px 15px'; - brandBar.style.display = 'flex'; - brandBar.style.alignItems = 'center'; - brandBar.style.justifyContent = 'space-between'; - brandBar.style.borderBottom = '1px solid rgba(255,255,255,0.05)'; - - const badge = document.createElement('div'); - badge.style.background = 'linear-gradient(135deg, #444, #222)'; - badge.style.color = '#ccc'; - badge.style.fontSize = '0.7em'; - badge.style.padding = '4px 8px'; - badge.style.borderRadius = '6px'; - badge.style.letterSpacing = '1px'; - badge.style.border = '1px solid #555'; - badge.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)'; - badge.style.textTransform = 'uppercase'; - badge.style.fontWeight = 'bold'; - badge.innerHTML = 'Anomalous Browser'; - - const menuBtn = document.createElement('button'); - menuBtn.innerHTML = '☰'; - menuBtn.title = 'Toggle Sidebar'; - menuBtn.style.background = 'transparent'; - menuBtn.style.border = 'none'; - menuBtn.style.color = '#ccc'; - menuBtn.style.fontSize = '1.2em'; - menuBtn.style.cursor = 'pointer'; - menuBtn.onclick = () => { - if (container.classList.contains('anomalous-sidebar-closed')) { - container.classList.remove('anomalous-sidebar-closed'); - localStorage.setItem('anomalous_user_sidebar_closed', 'false'); - } else { - container.classList.add('anomalous-sidebar-closed'); - localStorage.setItem('anomalous_user_sidebar_closed', 'true'); - } - }; - - brandBar.appendChild(badge); - brandBar.appendChild(menuBtn); - - this.sidebar = document.createElement('div'); - this.sidebar.id = 'anomalous-sidebar'; - - this.sidebarActions = document.createElement('div'); - this.sidebarActions.id = 'anomalous-sidebar-actions'; - this.sidebarActions.style.padding = '10px 15px'; - this.sidebarActions.style.display = 'flex'; - this.sidebarActions.style.flexDirection = 'row'; - this.sidebarActions.style.justifyContent = 'flex-start'; - this.sidebarActions.style.alignItems = 'center'; - this.sidebarActions.style.gap = '10px'; - this.sidebarActions.style.borderTop = '1px solid rgba(255,255,255,0.05)'; - this.sidebarActions.style.background = 'transparent'; - this.sidebarActions.style.borderRadius = '0'; - this.sidebarActions.style.width = '100%'; - this.sidebarActions.style.boxSizing = 'border-box'; - this.sidebarActions.style.margin = '0'; - - this.sidebarWrapper.appendChild(brandBar); - this.sidebarWrapper.appendChild(this.sidebar); - this.sidebarWrapper.appendChild(this.sidebarActions); - - // Content Area - const content = document.createElement('div'); - content.id = 'anomalous-content'; - - const header = document.createElement('div'); - header.id = 'anomalous-header'; - - let isDragging = false; - let dragOffsetX = 0; - let dragOffsetY = 0; - - const enforceBounds = (x, y) => { - let newX = x; - let newY = y; - if (newX + container.offsetWidth > window.innerWidth) newX = window.innerWidth - container.offsetWidth; - if (newY + container.offsetHeight > window.innerHeight) newY = window.innerHeight - container.offsetHeight; - if (newX < 0) newX = 0; - if (newY < 0) newY = 0; - return { x: newX, y: newY }; - }; - - header.addEventListener('mousedown', (e) => { - if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.id === 'anomalous-close') return; - isDragging = true; - const rect = container.getBoundingClientRect(); - dragOffsetX = e.clientX - rect.left; - dragOffsetY = e.clientY - rect.top; - e.preventDefault(); - }); - - window.addEventListener('mousemove', (e) => { - if (!isDragging) return; - const pos = enforceBounds(e.clientX - dragOffsetX, e.clientY - dragOffsetY); - container.style.left = pos.x + 'px'; - container.style.top = pos.y + 'px'; - container.style.transform = 'none'; - }); - - window.addEventListener('mouseup', () => { - if (isDragging) { - isDragging = false; - localStorage.setItem('anomalous_pos_x', container.style.left); - localStorage.setItem('anomalous_pos_y', container.style.top); - } - }); - - const savedX = localStorage.getItem('anomalous_pos_x'); - const savedY = localStorage.getItem('anomalous_pos_y'); - if (savedX && savedY) { - container.style.left = savedX; - container.style.top = savedY; - } - - // Periodically enforce bounds to catch resize/zoom changes - setInterval(() => { - if (!isDragging && container.style.display !== 'none' && !container.classList.contains('anomalous-docked')) { - const rect = container.getBoundingClientRect(); - const pos = enforceBounds(rect.left, rect.top); - if (pos.x !== rect.left || pos.y !== rect.top) { - if (container.style.left.endsWith('px') && container.style.top.endsWith('px')) { - container.style.left = pos.x + 'px'; - container.style.top = pos.y + 'px'; - } - } - } - }, 1000); - - const spacer = document.createElement('div'); - spacer.style.flex = '1 1 auto'; - - const leftGroup = document.createElement('div'); - leftGroup.className = 'anomalous-header-group'; - - const rightGroup = document.createElement('div'); - rightGroup.className = 'anomalous-header-group'; - - const 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.currentDetailObserver) { - this.currentDetailObserver.disconnect(); - this.currentDetailObserver = null; - } - }; - - const showSidebar = () => { - container.classList.remove('anomalous-sidebar-closed'); - }; - - const modelsBtn = document.createElement('button'); - modelsBtn.id = 'anomalous-models-btn'; - modelsBtn.innerHTML = `🏠 ${t('models')}`; - modelsBtn.onclick = () => { - hideAllPanels(); - if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') { - container.classList.add('anomalous-sidebar-closed'); - } else { - showSidebar(); - } - menuBtn.disabled = false; - menuBtn.style.opacity = '1'; - menuBtn.style.cursor = 'pointer'; - this.grid.style.display = 'grid'; - if (this.detailPanel.innerHTML !== '') { - this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; - this.currentDetailModel = null; - this.historyStack = []; - } - }; - - const galleryBtn = document.createElement('button'); - galleryBtn.innerHTML = `🖼️ ${t('gallery') || '图库'}`; - galleryBtn.onclick = () => { - hideAllPanels(); - container.classList.add('anomalous-sidebar-closed'); - menuBtn.disabled = true; - menuBtn.style.opacity = '0.3'; - menuBtn.style.cursor = 'not-allowed'; - this.galleryPanel.style.display = 'flex'; - if (!this.galleryLoaded) { - this.loadGalleryImages(1, true); - } - }; - - const dockBtn = document.createElement('button'); - dockBtn.innerHTML = '◧'; - dockBtn.title = t('dockTitle'); - dockBtn.onclick = () => { - container.classList.toggle('anomalous-docked'); - if (container.classList.contains('anomalous-docked')) { - localStorage.setItem('anomalous_docked', 'true'); - } else { - localStorage.setItem('anomalous_docked', 'false'); - } - }; - - if (localStorage.getItem('anomalous_docked') === 'true') { - container.classList.add('anomalous-docked'); - } - - const helpBtn = document.createElement('button'); - helpBtn.id = 'anomalous-help-btn'; - helpBtn.title = t('helpTitle'); - helpBtn.innerHTML = `❓ ${t('help')}`; - helpBtn.onclick = () => this.showHelp(); - - const nbBtn = document.createElement('button'); - nbBtn.id = 'anomalous-notebook-btn'; - nbBtn.title = t('notebookTitle'); - nbBtn.innerHTML = `📑 ${t('notebooks')}`; - nbBtn.onclick = () => { - this.nbPanel.style.display = 'flex'; - this.showNotebooks(); - }; - - rightGroup.appendChild(modelsBtn); - rightGroup.appendChild(galleryBtn); - rightGroup.appendChild(nbBtn); - - const doctorBtn = document.createElement('button'); - doctorBtn.innerHTML = `🩺 ${currentLang === 'zh' ? '模型医生' : 'Model Doctor'}`; - doctorBtn.onmouseout = () => { doctorBtn.style.background = 'transparent'; doctorBtn.style.color = '#ccc'; }; - doctorBtn.onclick = () => { - 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.doctorPanel.style.display = 'flex'; - if (!this.doctorPanelInitialized) { - this.initDoctorPanel(); - } - }; - rightGroup.appendChild(doctorBtn); - - const apiKeyBtn = document.createElement('button'); - apiKeyBtn.id = 'anomalous-api-btn'; - apiKeyBtn.innerHTML = `${t('apiKeyConfig')}`; - apiKeyBtn.onclick = async () => { - const current = localStorage.getItem('anomalous_api_key') || ''; - const val = prompt(t('apiKeyPrompt'), current); - if (val !== null) { - localStorage.setItem('anomalous_api_key', val.trim()); - 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 = currentLang === '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(); - alert(currentLang === 'zh' ? '✅ 扫描/重命名已完成!数据已为您更新。' : '✅ Scan/Rename completed! Data updated.'); - } - } catch (e) { } - }, 3000); - - const createWizardModal = (isGlobal = false) => { - let wizard = document.getElementById('anomalous-wizard-modal'); - if (wizard) document.body.removeChild(wizard); - - wizard = document.createElement('div'); - wizard.id = 'anomalous-wizard-modal'; - wizard.style.position = 'fixed'; - wizard.style.top = '0'; - wizard.style.left = '0'; - wizard.style.width = '100vw'; - wizard.style.height = '100vh'; - wizard.style.backgroundColor = 'rgba(0,0,0,0.6)'; - wizard.style.zIndex = '999999'; - wizard.style.display = 'flex'; - wizard.style.justifyContent = 'center'; - wizard.style.alignItems = 'center'; - wizard.style.fontFamily = 'Roboto, "Segoe UI", sans-serif'; - - const content = document.createElement('div'); - content.style.background = '#1E1E1E'; - content.style.borderRadius = '12px'; - content.style.padding = '32px'; - content.style.width = '760px'; - content.style.maxWidth = '95%'; - content.style.maxHeight = '90vh'; - content.style.overflowY = 'auto'; - content.style.boxShadow = '0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12)'; - content.style.color = '#fff'; - content.style.position = 'relative'; - - // Top Header Area - const headerArea = document.createElement('div'); - headerArea.style.display = 'flex'; - headerArea.style.justifyContent = 'space-between'; - headerArea.style.alignItems = 'center'; - headerArea.style.marginBottom = '24px'; - - const title = document.createElement('h2'); - title.innerText = currentLang === 'zh' - ? (isGlobal ? '全局扫描向导' : '扫描向导') - : (isGlobal ? 'Global Scan Wizard' : 'Scan Wizard'); - title.style.margin = '0'; - title.style.fontSize = '1.6em'; - title.style.fontWeight = '500'; - - // Toolbar right - const topToolbar = document.createElement('div'); - topToolbar.style.display = 'flex'; - topToolbar.style.gap = '8px'; - - const createGhostBtn = (icon, textZh, textEn, onClick) => { - const btn = document.createElement('button'); - btn.innerHTML = `${icon} ${currentLang === '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 currentKey = localStorage.getItem('anomalous_civitai_api_key') || ''; - const newKey = prompt(currentLang === 'zh' ? '请输入 Civitai API Key (留空取消):' : 'Enter Civitai API Key:', currentKey); - if (newKey !== null) { - localStorage.setItem('anomalous_civitai_api_key', newKey.trim()); - alert(currentLang === 'zh' ? '✅ 密钥保存成功' : '✅ Key Saved'); - } - }); - - const missingBtn = createGhostBtn('🚨', '查找缺失模型', 'Find Missing', () => { - if (window.anomalous_resolve_all_missing_nodes) { - window.anomalous_resolve_all_missing_nodes(true, true); - document.body.removeChild(wizard); - } else { - alert(currentLang === 'zh' ? "功能未加载!" : "Module not loaded!"); - } - }); - - topToolbar.appendChild(apiKeyBtn); - topToolbar.appendChild(missingBtn); - - headerArea.appendChild(title); - headerArea.appendChild(topToolbar); - content.appendChild(headerArea); - - let scanMode = 'civitai'; - let enableRename = true; - let enableAutoCheck = true; - let updateSections = () => {}; - - const formGroup = document.createElement('div'); - formGroup.style.display = 'flex'; - formGroup.style.flexDirection = 'column'; - formGroup.style.gap = '24px'; - - // === Step 1: Fetch Data === - const section1 = document.createElement('div'); - const sec1Title = document.createElement('div'); - sec1Title.innerText = currentLang === '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'; - - 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 = currentLang === '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 = currentLang === '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 card1, card2; - const updateCards = () => { - if (card1 && card2) { - cardsContainer.removeChild(card1); - cardsContainer.removeChild(card2); - } - card1 = createChoiceCard('civitai', '🌍', '在线完整匹配', 'Online Full Matching', '自动连接 Civitai 平台,深度获取模型封面图、触发词 (Trigger Words)、作者备注及详细标签。推荐日常使用。', 'Connects to Civitai to fetch comprehensive model covers, trigger words, author notes, and tags. Recommended for daily use.', scanMode === 'civitai'); - card2 = createChoiceCard('offline', '🔌', '离线极速读取', 'Offline Fast Read', '不消耗任何网络。仅通过本地张量计算极速提取模型内的基础元数据,速度极快但信息较基础。', 'No network required. Extremely fast extraction of basic metadata via local tensor computation, but info is limited.', scanMode === 'offline'); - - card1.onclick = () => { scanMode = 'civitai'; updateCards(); updateSections(); }; - card2.onclick = () => { scanMode = 'offline'; updateCards(); updateSections(); }; - - cardsContainer.appendChild(card1); - cardsContainer.appendChild(card2); - }; - updateCards(); - section1.appendChild(cardsContainer); - formGroup.appendChild(section1); - - // Material Switch Builder - const createMaterialSwitch = (initialState, onChange) => { - const track = document.createElement('div'); - track.style.width = '36px'; - track.style.height = '14px'; - track.style.borderRadius = '7px'; - track.style.background = initialState ? 'rgba(138, 180, 248, 0.5)' : 'rgba(255,255,255,0.3)'; - track.style.position = 'relative'; - track.style.cursor = 'pointer'; - track.style.transition = 'background 0.3s'; - track.style.display = 'flex'; - track.style.alignItems = 'center'; - - const thumb = document.createElement('div'); - thumb.style.width = '20px'; - thumb.style.height = '20px'; - thumb.style.borderRadius = '50%'; - thumb.style.background = initialState ? '#8AB4F8' : '#bdbdbd'; - thumb.style.position = 'absolute'; - thumb.style.left = initialState ? '16px' : '0px'; - thumb.style.transition = 'left 0.3s, background 0.3s'; - thumb.style.boxShadow = '0 1px 3px rgba(0,0,0,0.4)'; - - let state = initialState; - track.onclick = () => { - state = !state; - track.style.background = state ? 'rgba(138, 180, 248, 0.5)' : 'rgba(255,255,255,0.3)'; - thumb.style.background = state ? '#8AB4F8' : '#bdbdbd'; - thumb.style.left = state ? '16px' : '0px'; - onChange(state); - }; - track.appendChild(thumb); - return track; - }; - - const createListRow = (icon, titleZh, titleEn, descZh, descEn, actionEl) => { - const row = document.createElement('div'); - row.style.display = 'flex'; - row.style.alignItems = 'center'; - row.style.justifyContent = 'space-between'; - row.style.padding = '12px 0'; - row.style.borderBottom = '1px solid rgba(255,255,255,0.05)'; - - const left = document.createElement('div'); - left.style.display = 'flex'; - left.style.alignItems = 'flex-start'; - left.style.gap = '16px'; - - const iconEl = document.createElement('div'); - iconEl.innerText = icon; - iconEl.style.fontSize = '1.4em'; - iconEl.style.lineHeight = '1.2'; - iconEl.style.width = '24px'; - iconEl.style.textAlign = 'center'; - - const textDiv = document.createElement('div'); - const t = document.createElement('div'); - t.innerText = currentLang === 'zh' ? titleZh : titleEn; - t.style.fontWeight = '500'; - t.style.fontSize = '1.0em'; - t.style.color = '#fff'; - - const d = document.createElement('div'); - d.innerHTML = currentLang === 'zh' ? descZh : descEn; - d.style.fontSize = '0.85em'; - d.style.color = '#aaa'; - d.style.marginTop = '4px'; - - textDiv.appendChild(t); - textDiv.appendChild(d); - left.appendChild(iconEl); - left.appendChild(textDiv); - row.appendChild(left); - if (actionEl) row.appendChild(actionEl); - - return row; - }; - - // === Step 2: Normalize Naming === - const section2 = document.createElement('div'); - const sec2Title = document.createElement('div'); - sec2Title.innerText = currentLang === '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 = `✨ ${currentLang === 'zh' ? '虚拟重命名 (安全推荐)' : 'Virtual Rename (Safe)'}`; - vRow.appendChild(virtualSwitch); - - const vDesc = document.createElement('div'); - vDesc.style.fontSize = '0.8em'; - vDesc.style.color = '#888'; - vDesc.style.marginTop = '4px'; - vDesc.innerHTML = currentLang === '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 = `💾 ${currentLang === 'zh' ? '物理重命名' : 'Physical Rename'}`; - pRow.appendChild(physicalSwitch); - - const pDesc = document.createElement('div'); - pDesc.style.fontSize = '0.8em'; - pDesc.style.color = '#888'; - pDesc.style.marginTop = '4px'; - pDesc.innerHTML = currentLang === 'zh' ? '真实修改底层的 .safetensors 及其所有附属文件名,彻底告别乱码文件名。' : 'Permanently rename the underlying .safetensors and associated files on disk.'; - pContainer.appendChild(pRow); - pContainer.appendChild(pDesc); - - dualChannelRow.appendChild(vContainer); - dualChannelRow.appendChild(pContainer); - - s2List.appendChild(createListRow('📝', '自动规范命名', 'Auto-Normalize Naming', '强迫症福音。自动将杂乱无章的模型文件名重命名为 模型名_版本名.safetensors 的标准格式。
例如: model_final(1).safetensors ➔ Beautiful_Mix_V1.0.safetensors', 'Automatically rename messy model files to the official standard format: ModelName_VersionName.safetensors.
e.g. model_final(1).safetensors ➔ Beautiful_Mix_V1.0.safetensors', createMaterialSwitch(enableRename, (s) => { enableRename = s; updateDualChannelUI(); }))); - s2List.lastChild.style.borderBottom = 'none'; - s2List.appendChild(dualChannelRow); - updateDualChannelUI(); - section2.appendChild(s2List); - formGroup.appendChild(section2); - - // === Step 3: Workflow Protection & Fix === - const section3 = document.createElement('div'); - const sec3Title = document.createElement('div'); - sec3Title.innerText = currentLang === '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('🪄', '智能修复当前工作流', 'Smart Fix Current Workflow', '扫描完成后,自动尝试用最新数据修复当前画布中提示缺失的报错模型节点。', 'After scanning, automatically attempt to fix missing model errors on the current canvas.', createMaterialSwitch(enableAutoCheck, (s) => enableAutoCheck = s))); - s3List.lastChild.style.borderBottom = 'none'; - - section3.appendChild(s3List); - formGroup.appendChild(section3); - content.appendChild(formGroup); - - updateSections = () => { - if (scanMode === 'offline') { - section2.style.opacity = '0.3'; - section2.style.pointerEvents = 'none'; - section3.style.opacity = '0.3'; - section3.style.pointerEvents = 'none'; - } else { - section2.style.opacity = '1'; - section2.style.pointerEvents = 'auto'; - section3.style.opacity = '1'; - section3.style.pointerEvents = 'auto'; - } - }; - updateSections(); - - // Execute Scan Logic - const doScan = async () => { - try { - const reqBody = { - offline_only: scanMode === 'offline', - skip_rename: !enableRename, - virtual_rename: enableRename ? enableVirtualRename : false, - physical_rename: enableRename ? enablePhysicalRename : false - }; - - let targetUrl = ''; - if (isGlobal) { - targetUrl = '/anomalous/scan_all'; - } else { - const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder }); - targetUrl = '/anomalous/scan?' + params.toString(); - } - - const res = await fetch(targetUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(reqBody) - }); - - const data = await res.json(); - if (data.status === 'ok') { - alert(currentLang === 'zh' ? '🚀 扫描已在后台点火!稍后列表将自动刷新。' : '🚀 Scan ignited in background!'); - - if (enableAutoCheck && window.anomalous_resolve_all_missing_nodes) { - setTimeout(() => { - window.anomalous_resolve_all_missing_nodes(true, true); - }, 3000); - } - - this.loadModels(); - } - } 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 = currentLang === '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 = currentLang === 'zh' ? '执行扫描' : 'Execute Scan'; - startBtn.style.padding = '8px 24px'; - startBtn.style.background = '#8AB4F8'; // Material Primary - startBtn.style.color = '#1E1E1E'; - startBtn.style.border = 'none'; - startBtn.style.borderRadius = '4px'; - startBtn.style.cursor = 'pointer'; - startBtn.style.fontSize = '0.95em'; - startBtn.style.fontWeight = '500'; - startBtn.style.textTransform = 'uppercase'; - startBtn.style.transition = 'background 0.2s, box-shadow 0.2s'; - startBtn.style.boxShadow = '0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)'; - startBtn.onmouseover = () => { - startBtn.style.background = '#aecbf9'; - startBtn.style.boxShadow = '0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12)'; - }; - startBtn.onmouseout = () => { - startBtn.style.background = '#8AB4F8'; - startBtn.style.boxShadow = '0 3px 1px -2px rgba(0,0,0,0.2), 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)'; - }; - startBtn.onclick = doScan; - - footer.appendChild(closeBtn); - footer.appendChild(startBtn); - content.appendChild(footer); - wizard.appendChild(content); - document.body.appendChild(wizard); - }; - - scanBtn.onclick = () => createWizardModal(); - this.sidebarActions.appendChild(scanBtn); - - const energyBtn = document.createElement('button'); - energyBtn.id = 'anomalous-energy-btn'; - energyBtn.title = t('togglePlayTitle'); - const renderEnergyBtn = () => { - energyBtn.innerHTML = this.energySaving - ? `${t('eco')}` - : `${t('autoPlay')}`; - }; - renderEnergyBtn(); - energyBtn.onclick = () => { - this.energySaving = !this.energySaving; - localStorage.setItem('anomalous_energy_saving', this.energySaving); - renderEnergyBtn(); - this.loadModels(); - }; - - const closeBtn = document.createElement('div'); - closeBtn.id = 'anomalous-close'; - closeBtn.innerHTML = '×'; - closeBtn.onclick = () => this.close(); - - const cleanBtn = document.createElement('button'); - cleanBtn.id = 'anomalous-clean-btn'; - cleanBtn.title = t('cleanTitle'); - cleanBtn.innerHTML = `${t('clean')}`; - - cleanBtn.onclick = async () => { - if (!confirm(t('cleanConfirm'))) return; - cleanBtn.innerHTML = `⏳ ${t('cleaning')}`; - cleanBtn.disabled = true; - try { - const res = await fetch('/anomalous/clean_civitai_info', { method: 'POST' }); - const data = await res.json(); - if (data.status === 'success') { - alert(`${t('cleanDone')} ${data.count} ${t('files')}`); - } else { - alert(t('cleanFail') + data.message); - } - } catch (e) { alert(t('cleanErr') + e.message); } - cleanBtn.innerHTML = `${t('clean')}`; - cleanBtn.disabled = false; - }; - - 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 = currentLang === 'zh' ? '🌐 Language: EN' : '🌐 语言: 中文'; - langBtn.onclick = () => { - currentLang = currentLang === 'zh' ? 'en' : 'zh'; - localStorage.setItem('anomalous_lang', currentLang); - window.anomalous_browser_lang = currentLang; - langBtn.innerHTML = currentLang === 'zh' ? '🌐 Language: EN' : '🌐 语言: 中文'; - updateLangClass(); - modelsBtn.innerHTML = `🏠 ${t('models')}`; - galleryBtn.innerHTML = `🖼️ ${t('gallery')}`; - scanBtn.title = t('scanTitle'); - scanBtn.innerHTML = `🔄`; - cleanBtn.title = t('cleanTitle'); - cleanBtn.innerHTML = `${t('clean')}`; - helpBtn.innerHTML = `❓ ${t('help')}`; - nbBtn.title = t('notebookTitle'); - nbBtn.innerHTML = `📑 ${t('notebooks')}`; - apiKeyBtn.innerHTML = `${t('apiKeyConfig')}`; - const globalScanBtnRef = document.getElementById('anomalous-global-scan-btn'); - if (globalScanBtnRef) globalScanBtnRef.innerHTML = currentLang === 'zh' ? '🌍 一键全盘极速扫描 (不下载封面/不改名)' : '🌍 Global Quick Scan (No rename/No media)'; - const disableAutoScanBtnRef = document.getElementById('anomalous-disable-auto-scan-btn'); - if (disableAutoScanBtnRef) { - let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true'; - disableAutoScanBtnRef.innerHTML = currentLang === 'zh' - ? (isAutoEnabled ? '🛎️ 启动时自动检测缺失模型: [已开启]' : '🔕 启动时自动检测缺失模型: [已关闭]') - : (isAutoEnabled ? '🛎️ Auto-Detect Missing on Load: ON' : '🔕 Auto-Detect Missing on Load: OFF'); - } - const missingBtnRef = document.getElementById('anomalous-missing-btn'); - if (missingBtnRef) missingBtnRef.innerHTML = currentLang === 'zh' ? '🚨 查找缺失模型' : '🚨 Find Missing Models'; - const checkUnscannedBtnRef = document.getElementById('anomalous-check-unscanned-btn'); - if (checkUnscannedBtnRef) checkUnscannedBtnRef.innerHTML = currentLang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info'; - const resetBtnRef = document.getElementById('anomalous-reset-btn'); - if (resetBtnRef) resetBtnRef.innerHTML = currentLang === 'zh' ? '🔄 重置界面布局' : '🔄 Reset Layout'; - const scaleLabelRef = document.getElementById('anomalous-scale-label'); - if (scaleLabelRef) scaleLabelRef.innerText = currentLang === 'zh' ? 'UI 缩放' : 'UI Scale'; - - renderEnergyBtn(); - 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(cleanBtn); - styleHubBtn(energyBtn); - styleHubBtn(apiKeyBtn); - styleHubBtn(langBtn); - styleHubBtn(helpBtn); - - const globalScanBtn = document.createElement('button'); - globalScanBtn.id = 'anomalous-global-scan-btn'; - globalScanBtn.innerHTML = currentLang === 'zh' ? '🌍 一键全盘极速扫描 (不下载封面/不改名)' : '🌍 Global Quick Scan (No rename/No media)'; - styleHubBtn(globalScanBtn); - - globalScanBtn.onclick = async () => { - if (!confirm(currentLang === 'zh' ? '即将执行极速全盘扫描并同步Hash,此操作不改名也不下载封面,是否继续?' : 'Start global quick scan to sync all hashes?')) return; - globalScanBtn.innerHTML = currentLang === 'zh' ? '⏳ 扫描中...' : '⏳ Scanning...'; - globalScanBtn.disabled = true; - try { - const res = await fetch('/anomalous/scan_all', { method: 'POST' }); - const data = await res.json(); - if (data.status === 'ok') { - alert(currentLang === '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 = currentLang === 'zh' ? '✅ 扫描完成' : '✅ Scan Complete'; - setTimeout(() => { - globalScanBtn.innerHTML = currentLang === 'zh' ? '🌍 一键全盘扫描 (不下载封面/不改名)' : '🌍 Global Quick Scan (No rename/No media)'; - globalScanBtn.disabled = false; - }, 3000); - } - } catch (e) { } - }, 3000); - } else { - alert((currentLang === 'zh' ? '错误: ' : 'Error: ') + data.message); - globalScanBtn.disabled = false; - } - } catch (e) { - globalScanBtn.disabled = false; - } - }; - - const autoScanToggleBtn = document.createElement('button'); - autoScanToggleBtn.id = 'anomalous-disable-auto-scan-btn'; - const renderAutoScanToggleBtn = () => { - let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true'; - autoScanToggleBtn.innerHTML = currentLang === 'zh' - ? (isAutoEnabled ? '🛎️ 启动时自动检测缺失模型: [已开启]' : '🔕 启动时自动检测缺失模型: [已关闭]') - : (isAutoEnabled ? '🛎️ Auto-Detect Missing on Load: ON' : '🔕 Auto-Detect Missing on Load: OFF'); - }; - renderAutoScanToggleBtn(); - styleHubBtn(autoScanToggleBtn); - autoScanToggleBtn.onclick = () => { - let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true'; - localStorage.setItem('anomalous_auto_scan_enabled', isAutoEnabled ? 'false' : 'true'); - renderAutoScanToggleBtn(); - }; - - const missingBtn = document.createElement('button'); - missingBtn.id = 'anomalous-missing-btn'; - missingBtn.innerHTML = currentLang === 'zh' ? '🚨 查找缺失模型' : '🚨 Find Missing Models'; - styleHubBtn(missingBtn); - missingBtn.onclick = () => { - if (window.anomalous_resolve_all_missing_nodes) { - window.anomalous_resolve_all_missing_nodes(true, true); - } - }; - - const checkUnscannedBtn = document.createElement('button'); - checkUnscannedBtn.id = 'anomalous-check-unscanned-btn'; - checkUnscannedBtn.innerHTML = currentLang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info'; - styleHubBtn(checkUnscannedBtn); - checkUnscannedBtn.onclick = async () => { - checkUnscannedBtn.innerHTML = currentLang === '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 = currentLang === 'zh' ? '⚠️ 发现缺失,正在自动极速扫描...' : '⚠️ Missing info found, Auto-Scanning...'; - const scanRes = await fetch('/anomalous/scan_all', { method: 'POST' }); - 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 = currentLang === 'zh' ? '✅ 补全完成' : '✅ Info Complete'; - setTimeout(() => { - checkUnscannedBtn.innerHTML = currentLang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info'; - checkUnscannedBtn.disabled = false; - }, 3000); - } - } catch (e) { } - }, 3000); - } else { - alert((currentLang === 'zh' ? '错误: ' : 'Error: ') + scanData.message); - checkUnscannedBtn.disabled = false; - checkUnscannedBtn.innerHTML = currentLang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info'; - } - } else { - checkUnscannedBtn.innerHTML = currentLang === 'zh' ? '✨ 所有模型信息已完整' : '✨ All Model Info is Complete'; - setTimeout(() => { - checkUnscannedBtn.innerHTML = currentLang === 'zh' ? '🔍 检查并极速录入缺失模型信息' : '🔍 Check & Auto-Scan Missing Info'; - checkUnscannedBtn.disabled = false; - }, 3000); - } - } catch (e) { - checkUnscannedBtn.disabled = false; - checkUnscannedBtn.innerHTML = currentLang === '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 = currentLang === '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 = currentLang === 'zh' ? '🔄 重置界面布局' : '🔄 Reset Layout'; - styleHubBtn(resetBtn); - resetBtn.onclick = () => { - if (confirm(currentLang === '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 = currentLang === '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(); - }; - - // Many redundant buttons have been migrated to the Wizard! - settingsHubModal.appendChild(autoScanToggleBtn); - settingsHubModal.appendChild(cleanBtn); - settingsHubModal.appendChild(energyBtn); - settingsHubModal.appendChild(scaleContainer); - settingsHubModal.appendChild(langBtn); - settingsHubModal.appendChild(helpBtn); - settingsHubModal.appendChild(resetBtn); - - this.sidebarWrapper.appendChild(settingsHubModal); - - const settingsBtn = document.createElement('button'); - settingsBtn.innerHTML = `⚙️`; - settingsBtn.title = 'Settings Hub'; - settingsBtn.style.background = 'transparent'; - settingsBtn.style.color = '#ccc'; - settingsBtn.style.border = 'none'; - settingsBtn.style.borderRadius = '6px'; - settingsBtn.style.padding = '6px'; - settingsBtn.style.fontSize = '1.1em'; - settingsBtn.style.marginLeft = 'auto'; - settingsBtn.style.cursor = 'pointer'; - settingsBtn.style.transition = 'all 0.2s ease'; - settingsBtn.onmouseover = () => { settingsBtn.style.background = 'rgba(255,255,255,0.1)'; settingsBtn.style.color = '#fff'; }; - settingsBtn.onmouseout = () => { settingsBtn.style.background = 'transparent'; settingsBtn.style.color = '#ccc'; }; - const closeSettingsHub = (e) => { - if (settingsHubModal.style.display !== 'none' && !settingsHubModal.contains(e.target) && !settingsBtn.contains(e.target)) { - settingsHubModal.style.display = 'none'; - settingsBtn.style.color = '#ccc'; - document.removeEventListener('mousedown', closeSettingsHub); - } - }; - - settingsBtn.onclick = () => { - if (settingsHubModal.style.display === 'none') { - settingsHubModal.style.display = 'flex'; - settingsBtn.style.color = '#fff'; - // Delay adding the listener slightly to avoid triggering it on the same click - setTimeout(() => document.addEventListener('mousedown', closeSettingsHub), 10); - } else { - settingsHubModal.style.display = 'none'; - settingsBtn.style.color = '#ccc'; - document.removeEventListener('mousedown', closeSettingsHub); - } - }; - - const importBtn = document.createElement('button'); - importBtn.id = 'anomalous-import-btn'; - importBtn.title = currentLang === 'zh' ? '📥 预检导入工作流' : '📥 Pre-flight Import Workflow'; - 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 = () => { - this.showPreflightImportModal(); - }; - - 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.position = 'absolute'; - this.doctorPanel.style.top = '0'; - this.doctorPanel.style.left = '0'; - this.doctorPanel.style.width = '100%'; - this.doctorPanel.style.height = '100%'; - this.doctorPanel.style.background = 'rgba(15, 15, 15, 0.95)'; - this.doctorPanel.style.zIndex = '10000'; - this.doctorPanel.style.display = 'none'; - this.doctorPanel.style.flexDirection = 'column'; - this.doctorPanel.style.padding = '40px'; - this.doctorPanel.style.overflowY = 'auto'; - this.doctorPanel.style.boxSizing = 'border-box'; - this.doctorPanelInitialized = 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); - - container.appendChild(this.sidebarWrapper); - container.appendChild(content); - container.appendChild(this.nbPanel); - container.appendChild(this.doctorPanel); - - this.modal.appendChild(container); - - // Resize handle - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'anomalous-resize-handle'; - let isResizing = false; - resizeHandle.onmousedown = (e) => { - e.preventDefault(); - e.stopPropagation(); - isResizing = true; - }; - window.addEventListener('mousemove', (e) => { - if (!isResizing) return; - const rect = container.getBoundingClientRect(); - let newWidth = e.clientX - rect.left; - let newHeight = e.clientY - rect.top; - if (newWidth < 600) newWidth = 600; - if (newHeight < 400) newHeight = 400; - container.style.width = newWidth + 'px'; - container.style.height = newHeight + 'px'; - }); - window.addEventListener('mouseup', () => { - if (isResizing) { - isResizing = false; - localStorage.setItem('anomalous_width', container.style.width); - localStorage.setItem('anomalous_height', container.style.height); - } - }); - const savedW = localStorage.getItem('anomalous_width'); - const savedH = localStorage.getItem('anomalous_height'); - if (savedW) container.style.width = savedW; - if (savedH) container.style.height = savedH; - - container.appendChild(resizeHandle); - document.body.appendChild(this.modal); - } - - - 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); - } - - async loadFolders() { - try { - const res = await fetch('/anomalous/folders'); - const data = await res.json(); - this.foldersData = data.folders || []; - - if (!this.firstLoadDone && this.foldersData.length > 0) { - this.firstLoadDone = true; - let found = false; - for (const typeGroup of this.foldersData) { - const sortedPaths = Object.keys(typeGroup.folders).sort(); - for (const path of sortedPaths) { - if (typeGroup.folders[path].model_count > 0) { - this.currentType = typeGroup.type; - this.currentPathIdx = typeGroup.path_idx; - this.currentSubfolder = path; - found = true; - break; - } - } - if (found) break; - } - } - - // Auto expand all - (this.foldersData || []).forEach(typeGroup => { - this.expandedFolders.add(typeGroup.type); - Object.keys(typeGroup.folders).forEach(path => { - this.expandedFolders.add(typeGroup.type + path); - }); - }); - - this.renderSidebar(); - this.loadModels(); - } catch (e) { } - } - - 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 ? t('expandAll') : t('collapseAll'); - 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 = '🔍 搜索模型 (Search)...'; - 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) => { - 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 = `${typeGroup.label} ${isTypeExpanded ? '▼' : '▶'}`; - - header.onclick = () => { - if (isTypeExpanded) this.expandedFolders.delete(typeGroup.type); - else this.expandedFolders.add(typeGroup.type); - this.renderSidebar(); - }; - this.sidebar.appendChild(header); - - if (!isTypeExpanded) return; - - const sortedPaths = Object.keys(typeGroup.folders).sort(); - - sortedPaths.forEach(path => { - const info = typeGroup.folders[path]; - const parts = path.split('/').filter(p => p); - const parentPath = parts.length > 1 ? '/' + parts.slice(0, -1).join('/') : '/'; - - if (path !== '/' && parentPath !== '/') { - let parentId = typeGroup.type + parentPath; - if (!this.expandedFolders.has(parentId)) return; - } - - const hasChildren = sortedPaths.some(p => p !== path && p.startsWith(path === '/' ? '/' : path + '/')); - - const item = document.createElement('div'); - item.className = 'anomalous-folder-item'; - - const depth = path === '/' ? 0 : parts.length; - item.style.paddingLeft = (15 + depth * 15) + 'px'; - - const myId = typeGroup.type + path; - const isExpanded = this.expandedFolders.has(myId); - - let toggleIcon = ''; - if (hasChildren) { - toggleIcon = `${isExpanded ? '▼' : '▶'}`; - } else { - toggleIcon = ``; - } - - item.innerHTML = `${toggleIcon}${info.name} ${info.model_count}`; - - if (this.currentType === typeGroup.type && this.currentPathIdx === typeGroup.path_idx && this.currentSubfolder === path) { - item.classList.add('active'); - } - - item.onclick = (e) => { - if (e.target.classList.contains('anomalous-folder-toggle')) { - if (isExpanded) this.expandedFolders.delete(myId); - else this.expandedFolders.add(myId); - this.renderSidebar(); - return; - } - this.currentType = typeGroup.type; - this.currentPathIdx = typeGroup.path_idx; - this.currentSubfolder = path; - - this.detailPanel.style.display = 'none'; - this.grid.style.display = 'grid'; - - this.renderSidebar(); - this.loadModels(); - }; - - this.sidebar.appendChild(item); - }); - }); - } - - async loadModels() { - try { - const params = new URLSearchParams({ type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder }); - const res = await fetch('/anomalous/models?' + params.toString()); - const data = await res.json(); - - if (window.anomalous_update_hash_cache && data.models) { - window.anomalous_update_hash_cache(data.models); - } - - this.models = data.models || []; - const ts = Date.now(); - this.models.forEach(m => { - if (m.preview_url) { - m.preview_url += (m.preview_url.includes('?') ? '&' : '?') + 't=' + ts; - } - }); - - this.grid.innerHTML = ''; - - if (!data.models || data.models.length === 0) { - this.grid.innerHTML = `
${t('noModels')}
`; - return; - } - - data.models.forEach(model => { - const card = document.createElement('div'); - card.className = 'anomalous-card'; - if (model.preview_url) { - const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i); - if (isVideo) { - const video = document.createElement('video'); - video.src = model.preview_url; - video.muted = true; video.loop = true; video.playsInline = true; - if (this.energySaving) { - video.pause(); - card.addEventListener('mouseenter', () => video.play().catch(e => { })); - card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; }); - } else { - video.autoplay = true; - } - card.appendChild(video); - } else { - const img = document.createElement('img'); - img.src = model.preview_url; - card.appendChild(img); - } - } else { - const ph = document.createElement('div'); - ph.className = 'anomalous-card-placeholder'; - ph.innerHTML = `
${t('noPreview')}
${t('clickScan')}
`; - card.appendChild(ph); - } - const title = document.createElement('div'); - if (model.metadata && model.metadata.baseModel) { - const badge = document.createElement('div'); - badge.innerText = model.metadata.baseModel; - badge.style.position = 'absolute'; - badge.style.top = '6px'; - badge.style.left = '6px'; - badge.style.background = 'rgba(0,0,0,0.85)'; - badge.style.color = '#00ffcc'; - badge.style.padding = '3px 6px'; - badge.style.borderRadius = '4px'; - badge.style.fontSize = '0.75em'; - badge.style.fontWeight = 'bold'; - badge.style.border = '1px solid rgba(0,255,204,0.3)'; - badge.style.pointerEvents = 'none'; - badge.style.zIndex = '10'; - card.appendChild(badge); - } - title.className = 'anomalous-card-title'; - title.innerText = model.filename; - card.appendChild(title); - - card.onclick = () => { this.historyStack = []; this.currentDetailModel = model; this.showDetail(model); }; - - const applyBtn = document.createElement('button'); - applyBtn.innerHTML = '➕'; - applyBtn.title = t('applyToCanvas'); - applyBtn.style.position = 'absolute'; - applyBtn.style.top = '6px'; - applyBtn.style.right = '6px'; - applyBtn.style.background = 'rgba(0,0,0,0.7)'; - applyBtn.style.color = '#fff'; - applyBtn.style.border = '1px solid rgba(255,255,255,0.2)'; - applyBtn.style.borderRadius = '4px'; - applyBtn.style.cursor = 'pointer'; - applyBtn.style.padding = '4px 6px'; - applyBtn.style.zIndex = '20'; - applyBtn.style.fontSize = '1em'; - applyBtn.style.display = 'none'; - - const editBtn = document.createElement('button'); - editBtn.innerHTML = '⚙️'; - editBtn.title = window.anomalous_browser_lang === 'zh' ? '编辑模型 (Edit)' : 'Edit Model'; - editBtn.style.position = 'absolute'; - editBtn.style.top = '6px'; - editBtn.style.right = '40px'; - editBtn.style.width = '26px'; - editBtn.style.height = '26px'; - editBtn.style.borderRadius = '50%'; - editBtn.style.border = 'none'; - editBtn.style.background = 'rgba(0,0,0,0.7)'; - editBtn.style.color = '#fff'; - editBtn.style.cursor = 'pointer'; - editBtn.style.display = 'none'; - editBtn.style.alignItems = 'center'; - editBtn.style.justifyContent = 'center'; - editBtn.style.fontSize = '14px'; - editBtn.onclick = (e) => { - e.stopPropagation(); - this.showEditModal(model); - }; - card.appendChild(editBtn); - - card.addEventListener('mouseenter', () => { - applyBtn.style.display = 'block'; - editBtn.style.display = 'flex'; - }); - card.addEventListener('mouseleave', () => { - applyBtn.style.display = 'none'; - editBtn.style.display = 'none'; - }); - - applyBtn.onclick = (e) => { - e.stopPropagation(); - this.applyModelToCanvas(this.currentType, this.currentSubfolder, model); - }; - card.appendChild(applyBtn); - - - this.grid.appendChild(card); - }); - } catch (e) { } - } - - - applyModelToCanvas(type, subfolder, model) { - const nodeTypeMap = { - 'checkpoints': 'CheckpointLoaderSimple', - 'loras': 'LoraLoader', - 'unet': 'UNETLoader', - 'diffusion_models': 'UNETLoader' - }; - const nodeType = nodeTypeMap[type]; - if (!nodeType) { - alert(currentLang === 'zh' ? '不支持该类型模型的自动应用。' : 'Unsupported model type for auto-apply.'); - return; - } - - const node = LiteGraph.createNode(nodeType); - if (!node) { - alert((currentLang === 'zh' ? '创建节点失败: ' : 'Failed to create node: ') + nodeType); - return; - } - - if (app.canvas && app.canvas.graph_mouse) { - node.pos = [ - app.canvas.graph_mouse[0] || (window.innerWidth / 2), - app.canvas.graph_mouse[1] || (window.innerHeight / 2) - ]; - } else { - node.pos = [window.innerWidth / 2, window.innerHeight / 2]; - } - - app.graph.add(node); - - const sub = subfolder.replace(/^\/+/, '').replace(/\/+$/, ''); - const relPath = sub ? `${sub}/${model.filename}` : model.filename; - - this.setWidgetValuePath(node, relPath); - - this.close(); - - // 粘到鼠标上的逻辑 - let isSticking = true; - const stickHandler = (e) => { - if (!isSticking || !app.canvas) return; - const canvas = app.canvas; - - // LiteGraph内置了坐标转换,它会完美处理缩放和偏移带来的坐标偏移问题 - let canvasX, canvasY; - if (canvas.convertEventToCanvasOffset) { - const pos = canvas.convertEventToCanvasOffset(e); - canvasX = pos[0]; - canvasY = pos[1]; - } else { - // 如果API不可用,使用标准降级计算 - const rect = canvas.canvas.getBoundingClientRect(); - canvasX = (e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale; - canvasY = (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale; - } - - const w = (node.size && node.size[0]) ? node.size[0] : 200; - node.pos = [canvasX - w / 2, canvasY - 20]; - canvas.setDirty(true, true); - }; - const dropHandler = (e) => { - if (!isSticking) return; - isSticking = false; - window.removeEventListener('mousemove', stickHandler, true); - window.removeEventListener('pointerdown', dropHandler, true); - window.removeEventListener('mousedown', dropHandler, true); - window.removeEventListener('click', dropHandler, true); - e.preventDefault(); - e.stopPropagation(); - }; - window.addEventListener('mousemove', stickHandler, true); - setTimeout(() => { - window.addEventListener('pointerdown', dropHandler, true); - window.addEventListener('mousedown', dropHandler, true); - window.addEventListener('click', dropHandler, true); - }, 100); - } - - - stopMediaInContainer(container) { - if (!container) return; - const mediaElements = container.querySelectorAll('video, audio'); - mediaElements.forEach(media => { - media.pause(); - media.removeAttribute('src'); - media.load(); - }); - } - - showDetail(model) { - if (this.currentDetailObserver) { - this.currentDetailObserver.disconnect(); - this.currentDetailObserver = null; - } - this.grid.style.display = 'none'; - this.detailPanel.style.display = 'flex'; - this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; - - const header = document.createElement('div'); - header.style.width = '100%'; - header.style.padding = '8px 15px'; - header.style.background = 'var(--comfy-menu-bg, #333)'; - header.style.borderBottom = '1px solid var(--border-color, #444)'; - header.style.display = 'flex'; - header.style.alignItems = 'center'; - header.style.boxSizing = 'border-box'; - - const backBtn = document.createElement('button'); - let isFromDoctor = false; - if (this.historyStack.length > 0 && this.historyStack[this.historyStack.length - 1].type === 'doctor') { - isFromDoctor = true; - } - - if (isFromDoctor) { - backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回医生面板' : '⬅ Back to Doctor'; - backBtn.style.background = '#8AB4F8'; - backBtn.style.color = '#000'; - } else { - backBtn.innerHTML = this.historyStack.length > 0 ? t('backToPrev') : t('back'); - backBtn.style.background = '#444'; - backBtn.style.color = '#fff'; - } - backBtn.style.padding = '6px 12px'; - backBtn.style.border = 'none'; - backBtn.style.borderRadius = '4px'; - backBtn.style.cursor = 'pointer'; - backBtn.onclick = () => { - if (this.currentDetailObserver) { - this.currentDetailObserver.disconnect(); - this.currentDetailObserver = null; - } - if (this.historyStack.length > 0) { - const prev = this.historyStack.pop(); - if (prev.type === 'doctor') { - this.detailPanel.style.display = 'none'; - this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; - this.doctorPanel.style.display = 'flex'; - return; - } - this.currentType = prev.type; - this.currentPathIdx = prev.pathIdx; - this.currentSubfolder = prev.subfolder; - this.currentDetailModel = prev.model; - this.renderSidebar(); - this.showDetail(prev.model); - } else { - this.detailPanel.style.display = 'none'; - this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; - this.grid.style.display = 'grid'; - } - }; - - const title = document.createElement('h2'); - title.innerHTML = model.filename; - title.style.margin = '0 20px 0 20px'; - title.style.color = '#fff'; - title.style.fontSize = '1.2em'; - // 强制单行并溢出显示省略号 - title.style.whiteSpace = 'nowrap'; - title.style.overflow = 'hidden'; - title.style.textOverflow = 'ellipsis'; - title.style.flex = '1'; // 撑开剩余空间,把右侧按钮挤到最右边 - - const delBtn = document.createElement('button'); - delBtn.innerHTML = t('delModel'); - delBtn.style.padding = '6px 12px'; - delBtn.style.background = '#ff4444'; - delBtn.style.color = '#fff'; - delBtn.style.border = 'none'; - delBtn.style.borderRadius = '4px'; - delBtn.style.cursor = 'pointer'; - delBtn.style.marginLeft = 'auto'; // push to the right - delBtn.style.whiteSpace = 'nowrap'; - delBtn.onclick = async () => { - if (!confirm(`${t('delConfirm')} ${model.filename} ${t('delConfirm2')}`)) return; - try { - const res = await fetch('/anomalous/delete_model', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: this.currentType, - path_idx: this.currentPathIdx, - subfolder: this.currentSubfolder, - filename: model.filename - }) - }); - const data = await res.json(); - if (data.status === 'success') { - if (this.currentDetailObserver) { - this.currentDetailObserver.disconnect(); - this.currentDetailObserver = null; - } - alert(t('delSuccess') + data.deleted.join('\n') + t('delNote')); - this.detailPanel.style.display = 'none'; - this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = ''; - this.grid.style.display = 'grid'; - this.loadModels(); // refresh grid - } else { - alert(t('delFail') + data.message); - } - } catch (e) { - alert(t('delFail') + e.message); - } - }; - - const jumpBtn = document.createElement('button'); - jumpBtn.innerHTML = '⬇️'; - jumpBtn.title = t('jumpToBottom') || 'Jump to bottom'; - jumpBtn.style.padding = '6px 12px'; - jumpBtn.style.background = '#444'; - jumpBtn.style.color = '#fff'; - jumpBtn.style.border = 'none'; - jumpBtn.style.borderRadius = '4px'; - jumpBtn.style.cursor = 'pointer'; - jumpBtn.style.marginLeft = '10px'; - jumpBtn.onclick = () => { - // Find rightPanel which is created later, so we bind it dynamically - const rp = this.detailPanel.querySelector('.anomalous-split-right'); - if (rp) rp.scrollTo({ top: rp.scrollHeight, behavior: 'smooth' }); - }; - - header.appendChild(backBtn); - header.appendChild(title); - header.appendChild(delBtn); - header.appendChild(jumpBtn); - - const applyDetailBtn = document.createElement('button'); - applyDetailBtn.innerHTML = t('applyToCanvas') || 'Apply to Canvas'; - applyDetailBtn.style.padding = '6px 12px'; - applyDetailBtn.style.background = '#007bff'; - applyDetailBtn.style.color = '#fff'; - applyDetailBtn.style.border = 'none'; - applyDetailBtn.style.borderRadius = '4px'; - applyDetailBtn.style.cursor = 'pointer'; - applyDetailBtn.style.marginLeft = '10px'; - applyDetailBtn.style.fontWeight = 'bold'; - applyDetailBtn.onclick = () => { - this.applyModelToCanvas(this.currentType, this.currentSubfolder, model); - }; - header.appendChild(applyDetailBtn); - - - const splitContainer = document.createElement('div'); - splitContainer.className = 'anomalous-split-container'; - - const leftPanel = document.createElement('div'); - leftPanel.className = 'anomalous-split-left'; - - let isMediaRendered = null; - const renderMedia = (shouldRender) => { - if (shouldRender === isMediaRendered) return; - isMediaRendered = shouldRender; - leftPanel.innerHTML = ''; - if (!shouldRender) return; - - if (model.preview_url) { - const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i); - if (isVideo) { - const video = document.createElement('video'); - video.src = model.preview_url; - video.controls = true; - video.autoplay = true; - video.loop = true; - video.style.width = '100%'; - video.style.height = '100%'; - video.style.objectFit = 'contain'; - leftPanel.appendChild(video); - } else { - const img = document.createElement('img'); - img.src = model.preview_url; - img.style.width = '100%'; - img.style.height = '100%'; - img.style.objectFit = 'contain'; - leftPanel.appendChild(img); - } - } else { - leftPanel.innerHTML = `
${t('noPreview')}
`; - } - }; - - const containerEl = document.getElementById('anomalous-container'); - this.currentDetailObserver = new ResizeObserver(entries => { - for (let entry of entries) { - renderMedia(entry.contentRect.width >= 750); - } - }); - this.currentDetailObserver.observe(containerEl); - - const resizer = document.createElement('div'); - resizer.className = 'anomalous-resizer'; - - let isResizing = false; - resizer.addEventListener('mousedown', (e) => { - isResizing = true; - document.body.style.cursor = 'col-resize'; - e.preventDefault(); - }); - - window.addEventListener('mousemove', (e) => { - if (!isResizing) return; - const containerRect = splitContainer.getBoundingClientRect(); - let newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100; - if (newWidth < 20) newWidth = 20; - if (newWidth > 80) newWidth = 80; - leftPanel.style.width = newWidth + '%'; - rightPanel.style.width = (100 - newWidth) + '%'; - }); - - window.addEventListener('mouseup', () => { - if (isResizing) { - isResizing = false; - document.body.style.cursor = ''; - } - }); - - const rightPanel = document.createElement('div'); - rightPanel.className = 'anomalous-split-right'; - rightPanel.style.display = 'flex'; - rightPanel.style.flexDirection = 'column'; - rightPanel.style.height = '100%'; - rightPanel.style.boxSizing = 'border-box'; - rightPanel.style.overflow = 'auto'; - rightPanel.style.padding = '15px'; - rightPanel.style.color = '#eee'; - - const m = model.metadata || {}; - - // 1. Top Bar (Title + Size + Model + Button) in a single compact row if possible - const topRow = document.createElement('div'); - topRow.style.flexShrink = '0'; - topRow.style.display = 'flex'; - topRow.style.flexWrap = 'wrap'; - topRow.style.alignItems = 'center'; - topRow.style.gap = '10px'; - topRow.style.paddingBottom = '10px'; - topRow.style.marginBottom = '10px'; - topRow.style.borderBottom = '1px solid #444'; - - const titleEl = document.createElement('h3'); - titleEl.style.margin = '0'; - titleEl.style.fontSize = '1.3em'; - titleEl.style.marginRight = '10px'; - titleEl.innerText = m.custom_name || m.name || model.filename; - if (m.custom_name) { - titleEl.style.color = '#88ff88'; - } - topRow.appendChild(titleEl); - - const metaSpan = document.createElement('span'); - metaSpan.style.fontSize = '0.9em'; - metaSpan.style.color = '#aaa'; - metaSpan.innerHTML = `Size: ${model.size_mb} MB` + (m.baseModel ? ` Base: ${m.baseModel}` : ''); - topRow.appendChild(metaSpan); - - if (m.civitai_url) { - const cBtn = document.createElement('a'); - cBtn.href = m.civitai_url; - cBtn.target = '_blank'; - cBtn.innerHTML = '🌐 Civitai'; - cBtn.style.marginLeft = 'auto'; - cBtn.style.padding = '4px 8px'; - cBtn.style.background = '#1a73e8'; - cBtn.style.color = '#fff'; - cBtn.style.textDecoration = 'none'; - cBtn.style.borderRadius = '4px'; - cBtn.style.fontSize = '0.85em'; - cBtn.style.fontWeight = 'bold'; - topRow.appendChild(cBtn); - } - - const editMetaBtn = document.createElement('button'); - editMetaBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⚙️ 编辑' : '⚙️ Edit'; - editMetaBtn.style.marginLeft = m.civitai_url ? '10px' : 'auto'; - editMetaBtn.style.padding = '4px 8px'; - editMetaBtn.style.background = '#444'; - editMetaBtn.style.color = '#fff'; - editMetaBtn.style.border = 'none'; - editMetaBtn.style.borderRadius = '4px'; - editMetaBtn.style.fontSize = '0.85em'; - editMetaBtn.style.fontWeight = 'bold'; - editMetaBtn.style.cursor = 'pointer'; - editMetaBtn.onclick = () => { - this.showEditModal(model); - }; - topRow.appendChild(editMetaBtn); - - rightPanel.appendChild(topRow); - - // 1.5 Custom Notes Section (Google Material Card) - if (m.custom_notes) { - const notesCard = document.createElement('div'); - notesCard.style.flexShrink = '0'; - notesCard.style.marginBottom = '15px'; - notesCard.style.padding = '12px 16px'; - // Dark yellowish/khaki paper background for dark mode notebook feel - notesCard.style.background = 'linear-gradient(135deg, #262522 0%, #202124 100%)'; - notesCard.style.border = '1px solid #3c4043'; - notesCard.style.borderLeft = '4px solid #a38d53'; - notesCard.style.borderRadius = '4px 8px 8px 4px'; - notesCard.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'; - notesCard.style.position = 'relative'; - notesCard.style.fontFamily = 'Inter, Roboto, sans-serif'; - // Faint notebook lines background - notesCard.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)'; - notesCard.style.backgroundAttachment = 'local'; // ensures lines scroll with text - - const notesHeader = document.createElement('div'); - notesHeader.style.display = 'flex'; - notesHeader.style.justifyContent = 'space-between'; - notesHeader.style.alignItems = 'center'; - notesHeader.style.marginBottom = '8px'; - - const notesTitle = document.createElement('div'); - notesTitle.innerHTML = window.anomalous_browser_lang === 'zh' ? '📓 专属备注 (Notes)' : '📓 Custom Notes'; - notesTitle.style.color = '#a38d53'; - notesTitle.style.fontWeight = '600'; - notesTitle.style.fontSize = '0.85em'; - notesTitle.style.letterSpacing = '0.5px'; - - const notesEditBtn = document.createElement('button'); - notesEditBtn.innerHTML = '✏️'; - notesEditBtn.title = window.anomalous_browser_lang === 'zh' ? '编辑备注' : 'Edit Notes'; - notesEditBtn.style.background = 'transparent'; - notesEditBtn.style.border = 'none'; - notesEditBtn.style.color = '#a38d53'; - notesEditBtn.style.cursor = 'pointer'; - notesEditBtn.style.padding = '2px'; - notesEditBtn.style.fontSize = '1em'; - notesEditBtn.style.borderRadius = '50%'; - notesEditBtn.style.display = 'flex'; - notesEditBtn.style.alignItems = 'center'; - notesEditBtn.style.justifyContent = 'center'; - notesEditBtn.style.opacity = '0.7'; - notesEditBtn.onmouseover = () => notesEditBtn.style.opacity = '1'; - notesEditBtn.onmouseout = () => notesEditBtn.style.opacity = '0.7'; - notesEditBtn.onclick = () => { - this.showEditModal(model); - }; - - notesHeader.appendChild(notesTitle); - notesHeader.appendChild(notesEditBtn); - - const notesContent = document.createElement('div'); - notesContent.innerText = m.custom_notes; - notesContent.style.color = '#d1c9b4'; // Warm off-white - - notesContent.style.fontSize = '0.95em'; - notesContent.style.lineHeight = '24px'; // Matches the repeating gradient exactly - notesContent.style.whiteSpace = 'pre-wrap'; - notesContent.style.fontFamily = '"Consolas", "Courier New", monospace'; // Handwriting / typewriter feel - // Removed text shadow for cleaner look - - notesCard.appendChild(notesHeader); - notesCard.appendChild(notesContent); - - rightPanel.appendChild(notesCard); - } - // 1.8 Generated Gallery Button - const galleryBtnCont = document.createElement('div'); - galleryBtnCont.style.flexShrink = '0'; - galleryBtnCont.style.marginBottom = '15px'; - - const galleryBtn = document.createElement('button'); - galleryBtn.className = 'anomalous-nb-add-btn'; - galleryBtn.style.width = '100%'; - galleryBtn.style.display = 'flex'; - galleryBtn.style.justifyContent = 'center'; - galleryBtn.style.alignItems = 'center'; - galleryBtn.style.gap = '8px'; - galleryBtn.style.padding = '10px'; - galleryBtn.style.background = '#2a2b2f'; - galleryBtn.style.border = '1px solid #3c4043'; - galleryBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🖼️ 查看历史生成作品' : '🖼️ View Generated Gallery'; - galleryBtn.onmouseover = () => { galleryBtn.style.background = '#3c4043'; galleryBtn.style.borderColor = '#8ab4f8'; }; - galleryBtn.onmouseout = () => { galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.borderColor = '#3c4043'; }; - - galleryBtn.onclick = () => { - this.showGeneratedGallery(model); - }; - galleryBtnCont.appendChild(galleryBtn); - rightPanel.appendChild(galleryBtnCont); - - // 2. Trained Words Section - if (m.trainedWords && m.trainedWords.length > 0) { - const twCont = document.createElement('div'); - twCont.style.flexShrink = '0'; - twCont.style.marginBottom = '10px'; - - const twHeader = document.createElement('div'); - twHeader.style.display = 'flex'; - twHeader.style.alignItems = 'center'; - twHeader.style.marginBottom = '5px'; - - const twLabel = document.createElement('strong'); - twLabel.innerText = 'Trained Words:'; - twHeader.appendChild(twLabel); - - const copyAll = document.createElement('button'); - copyAll.innerText = t('copyAll'); - copyAll.style.marginLeft = '10px'; - copyAll.style.padding = '2px 6px'; - copyAll.style.background = '#444'; - copyAll.style.color = '#fff'; - copyAll.style.border = 'none'; - copyAll.style.borderRadius = '3px'; - copyAll.style.cursor = 'pointer'; - copyAll.style.fontSize = '0.8em'; - copyAll.onclick = () => { - const allWords = m.trainedWords.join(', '); - navigator.clipboard.writeText(allWords).then(() => { - const old = copyAll.innerText; - copyAll.innerText = t('copied'); - setTimeout(() => { copyAll.innerText = old; }, 1500); - }); - }; - twHeader.appendChild(copyAll); - twCont.appendChild(twHeader); - - const tagsCont = document.createElement('div'); - tagsCont.style.display = 'flex'; - tagsCont.style.flexWrap = 'wrap'; - tagsCont.style.gap = '4px'; - - m.trainedWords.forEach(w => { - const tag = document.createElement('span'); - tag.innerText = w; - tag.style.background = '#333'; - tag.style.padding = '2px 6px'; - tag.style.borderRadius = '4px'; - tag.style.fontSize = '0.85em'; - tag.style.cursor = 'pointer'; - tag.style.border = '1px solid #555'; - tag.title = t('clickToCopy') + w; - tag.onclick = () => { - navigator.clipboard.writeText(w).then(() => { - tag.style.background = '#28a745'; - setTimeout(() => { tag.style.background = '#333'; }, 500); - }); - }; - tagsCont.appendChild(tag); - }); - twCont.appendChild(tagsCont); - rightPanel.appendChild(twCont); - } - - // 3. Description Section (Expands to fill remaining height) - if (m.description) { - const descCont = document.createElement('div'); - descCont.style.flex = 'none'; - descCont.style.display = 'flex'; - descCont.style.flexDirection = 'column'; - // important for flex scroll - - const descLabel = document.createElement('strong'); - descLabel.innerText = 'Description:'; - descLabel.style.marginBottom = '5px'; - descCont.appendChild(descLabel); - - const descText = document.createElement('div'); - descText.style.flex = 'none'; - - descText.style.background = '#222'; - descText.style.padding = '10px'; - descText.style.borderRadius = '6px'; - descText.style.border = '1px solid #333'; - descText.style.fontSize = '0.95em'; - descText.style.lineHeight = '1.4'; - descText.innerHTML = m.description; - descCont.appendChild(descText); - - rightPanel.appendChild(descCont); - } - - // 4. Notes Section - if (m.notes) { - const notesCont = document.createElement('div'); - notesCont.style.flexShrink = '0'; - notesCont.style.marginTop = '10px'; - - const notesLabel = document.createElement('strong'); - notesLabel.innerText = 'Notes:'; - notesCont.appendChild(notesLabel); - - const notesText = document.createElement('div'); - notesText.style.background = '#332b00'; - notesText.style.padding = '8px'; - notesText.style.borderRadius = '6px'; - notesText.style.marginTop = '5px'; - notesText.style.border = '1px solid #554400'; - notesText.style.fontSize = '0.9em'; - notesText.innerHTML = m.notes; - notesCont.appendChild(notesText); - - rightPanel.appendChild(notesCont); - } - - // --- Compatible Models Section --- - if (m.baseModel) { - const compSec = document.createElement('div'); - compSec.className = 'anomalous-compatible-section'; - - const compTitle = document.createElement('div'); - compTitle.className = 'anomalous-compatible-title'; - compTitle.innerHTML = `${t('compatibleModels') || '🔗 Compatible'} (${m.baseModel})`; - - const compList = document.createElement('div'); - compList.className = 'anomalous-compatible-list'; - compList.innerHTML = `${t('loadingCompatible') || 'Loading...'}`; - - compSec.appendChild(compTitle); - compSec.appendChild(compList); - rightPanel.appendChild(compSec); - - const targetType = this.currentType === 'loras' ? 'checkpoints,unet,diffusion_models' : 'loras'; - fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(m.baseModel)}&target_type=${encodeURIComponent(targetType)}`) - .then(r => r.json()) - .then(d => { - compList.innerHTML = ''; - if (window.anomalous_update_hash_cache && d.models) { - window.anomalous_update_hash_cache(d.models); - } - if (!d.models || d.models.length === 0) { - compList.innerHTML = `No compatible models found.`; - return; - } - d.models.forEach(m_comp => { - const mItem = document.createElement('div'); - mItem.className = 'anomalous-compatible-item'; - mItem.title = m_comp.filename; - - let thumb = ''; - if (m_comp.preview_url) { - const isVid = m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i); - if (isVid) thumb = ``; - else thumb = ``; - } else { - thumb = `
?
`; - } - - mItem.innerHTML = `${thumb}
${m_comp.filename}
`; - - if (m_comp.preview_url && (m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i))) { - mItem.onmouseenter = () => { const v = mItem.querySelector('video'); if (v) v.play().catch(e => { }); }; - mItem.onmouseleave = () => { const v = mItem.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } }; - } - - mItem.onclick = () => { - this.historyStack.push({ - type: this.currentType, - pathIdx: this.currentPathIdx, - subfolder: this.currentSubfolder, - model: this.currentDetailModel - }); - - this.currentType = m_comp.type; - this.currentPathIdx = m_comp.path_idx; - this.currentSubfolder = m_comp.subfolder; - this.currentDetailModel = m_comp; - - this.renderSidebar(); - this.showDetail(m_comp); - }; - - compList.appendChild(mItem); - }); - }) - .catch(e => { - compList.innerHTML = `Failed to load.`; - }); - } - // --------------------------------- - - splitContainer.appendChild(leftPanel); - splitContainer.appendChild(resizer); - splitContainer.appendChild(rightPanel); - - this.detailPanel.appendChild(header); - this.detailPanel.appendChild(splitContainer); - } - - show() { - this.modal.classList.add('visible'); - if (!this.foldersData) { - this.loadFolders(); - } else { - this.loadModels(); - } - } - - close() { this.modal.classList.remove('visible'); } - - async showNotebooks() { - if (this.nbInitialized) { - this.refreshNotebooks(true); - return; - } - this.nbInitialized = true; - - this.nbPanel.innerHTML = ''; - - const nbContainer = document.createElement('div'); - nbContainer.className = 'anomalous-nb-container'; - - const nbHeader = document.createElement('div'); - nbHeader.className = 'anomalous-nb-header'; - nbHeader.innerHTML = `

${t('notebookTitle')}

`; - const closeNb = document.createElement('span'); - closeNb.className = 'anomalous-nb-close'; - closeNb.innerHTML = '×'; - closeNb.onclick = () => { this.nbPanel.style.display = 'none'; }; - nbHeader.appendChild(closeNb); - - const body = document.createElement('div'); - body.className = 'anomalous-nb-body'; - - // Sidebar for notebooks list - const sidebar = document.createElement('div'); - sidebar.className = 'anomalous-nb-sidebar'; - - const nbList = document.createElement('div'); - nbList.className = 'anomalous-nb-list'; - - const btnRow = document.createElement('div'); - btnRow.style.padding = '10px'; - btnRow.style.display = 'flex'; - btnRow.style.gap = '5px'; - - const createBtn = document.createElement('button'); - createBtn.innerHTML = `➕ ${t('createNotebook')}`; - createBtn.className = 'anomalous-btn-primary'; - - const createInput = document.createElement('input'); - createInput.type = 'text'; - createInput.placeholder = t('newNotebookName'); - createInput.style.display = 'none'; - createInput.style.flex = '1'; - createInput.style.padding = '4px'; - createInput.style.background = '#222'; - createInput.style.color = '#fff'; - createInput.style.border = '1px solid #555'; - createInput.style.borderRadius = '4px'; - - createBtn.onclick = () => { - if (createInput.style.display === 'none') { - createInput.style.display = 'block'; - createBtn.innerHTML = '✓'; - createInput.focus(); - } else { - const name = createInput.value.trim(); - if (name) { - this.currentNotebook = { filename: name + '.json', name: name, data: { baseModel: '', mainModel: null, loras: [], promptEn: '', promptZh: '' } }; - this.saveCurrentNotebook(); - this.renderNotebookEditor(); - createInput.value = ''; - } - createInput.style.display = 'none'; - createBtn.innerHTML = `➕ ${t('createNotebook')}`; - } - }; - - btnRow.appendChild(createInput); - btnRow.appendChild(createBtn); - sidebar.appendChild(btnRow); - sidebar.appendChild(nbList); - - // Editor area - this.nbEditor = document.createElement('div'); - this.nbEditor.className = 'anomalous-nb-editor'; - - body.appendChild(sidebar); - body.appendChild(this.nbEditor); - - nbContainer.appendChild(nbHeader); - nbContainer.appendChild(body); - - this.nbPanel.appendChild(nbContainer); - - this.nbListEl = nbList; - this.refreshNotebooks(true); - } - - async translateText(text) { - if (!text || !text.trim()) return ""; - try { - const res = await fetch('/anomalous/translate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: text, target_lang: currentLang === 'zh' ? 'zh-CN' : 'en' }) - }); - const data = await res.json(); - return data.translated || text; - } catch (e) { return text; } - } - - async loadGalleryImages(page = 1, reset = false) { - if (this.galleryLoading) return; - this.galleryLoading = true; - this.gallerySentinel.innerHTML = '加载中... / Loading...'; - - try { - const res = await fetch(`/anomalous/gallery_images?page=${page}&limit=50`); - const data = await res.json(); - - if (reset) { - // Clear existing cards - const cards = this.galleryGrid.querySelectorAll('.anomalous-gallery-card'); - cards.forEach(c => c.remove()); - this.galleryLoaded = true; - } - - if (data.images && data.images.length > 0) { - data.images.forEach(imgData => { - const card = document.createElement('div'); - card.className = 'anomalous-gallery-card'; - - const q_sub = encodeURIComponent(imgData.subfolder); - const q_file = encodeURIComponent(imgData.filename); - const imgUrl = `/view?filename=${q_file}&subfolder=${q_sub}&type=output`; - - const img = document.createElement('img'); - img.src = imgUrl; - img.loading = 'lazy'; - img.draggable = true; - - // Drag and drop support for ComfyUI - img.addEventListener('dragstart', (e) => { - const fullUrl = new URL(imgUrl, window.location.href).href; - e.dataTransfer.setData('text/uri-list', fullUrl); - e.dataTransfer.setData('text/plain', fullUrl); - - // Fix for Chromium failing to initiate drag for extremely large (Hires Fix) images - if (window.anomalousDragGhostImg) { - e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40); - } - }); - - // Click to view - img.onclick = () => { - if (this.gallerySelectModel) { - const model = this.gallerySelectModel; - fetch('/anomalous/set_custom_cover', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: this.currentType, - path_idx: this.currentPathIdx, - subfolder: this.currentSubfolder, - filename: model.filename, - source_image: imgData.subfolder ? imgData.subfolder + '/' + imgData.filename : imgData.filename - }) - }).then(res => res.json()).then(async data => { - if (data.status === 'success') { - const tempModel = this.gallerySelectModel; - this.gallerySelectModel = null; - const banner = document.getElementById('anomalous-gallery-select-banner'); - if (banner) banner.style.display = 'none'; - this.galleryPanel.style.display = 'none'; - - await this.loadModels(); - const updatedModel = this.models.find(m => m.filename === model.filename); - - if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) { - this.detailPanel.style.display = 'flex'; - if (updatedModel) this.showDetail(updatedModel); - } else { - this.grid.style.display = 'grid'; - // Grid was already refreshed by loadModels - } - - - } else { - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); - } - }); - return; - } - this.showGalleryViewer(imgUrl); - }; - - const delBtn = document.createElement('button'); - delBtn.className = 'anomalous-gallery-delete'; - delBtn.innerHTML = '🗑️'; - delBtn.title = 'Delete'; - - delBtn.onclick = (e) => { - e.stopPropagation(); - - const overlay = document.createElement('div'); - overlay.style.position = 'absolute'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100%'; - overlay.style.height = '100%'; - overlay.style.background = 'rgba(0,0,0,0.85)'; - overlay.style.display = 'flex'; - overlay.style.flexDirection = 'column'; - overlay.style.alignItems = 'center'; - overlay.style.justifyContent = 'center'; - overlay.style.gap = '15px'; - overlay.style.zIndex = '10'; - - const msg = document.createElement('div'); - msg.innerHTML = '彻底删除这张图片?
Delete permanently?'; - msg.style.color = '#fff'; - msg.style.fontWeight = 'bold'; - msg.style.textAlign = 'center'; - - const btnRow = document.createElement('div'); - btnRow.style.display = 'flex'; - btnRow.style.gap = '12px'; - - const confirmBtn = document.createElement('button'); - confirmBtn.innerHTML = currentLang === 'zh' ? '🗑️ 删除' : '🗑️ Delete'; - confirmBtn.style.background = '#dc3545'; - confirmBtn.style.color = '#fff'; - confirmBtn.style.border = 'none'; - confirmBtn.style.padding = '10px 16px'; - confirmBtn.style.borderRadius = '6px'; - confirmBtn.style.cursor = 'pointer'; - confirmBtn.style.fontWeight = 'bold'; - confirmBtn.style.transition = 'background 0.2s'; - confirmBtn.onmouseover = () => confirmBtn.style.background = '#ff0000'; - confirmBtn.onmouseout = () => confirmBtn.style.background = '#dc3545'; - - const cancelBtn = document.createElement('button'); - cancelBtn.innerHTML = '取消 (Cancel)'; - cancelBtn.style.background = '#444'; - cancelBtn.style.color = '#fff'; - cancelBtn.style.border = 'none'; - cancelBtn.style.padding = '10px 16px'; - cancelBtn.style.borderRadius = '6px'; - cancelBtn.style.cursor = 'pointer'; - cancelBtn.style.transition = 'background 0.2s'; - cancelBtn.onmouseover = () => cancelBtn.style.background = '#666'; - cancelBtn.onmouseout = () => cancelBtn.style.background = '#444'; - - cancelBtn.onclick = (ce) => { - ce.stopPropagation(); - overlay.remove(); - }; - - confirmBtn.onclick = async (ce) => { - ce.stopPropagation(); - confirmBtn.innerHTML = currentLang === 'zh' ? '删除中...' : 'Deleting...'; - confirmBtn.disabled = true; - try { - const dr = await fetch('/anomalous/delete_gallery_image', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename: imgData.filename, subfolder: imgData.subfolder }) - }); - const dd = await dr.json(); - if (dd.status === 'success') { - card.remove(); - } else { - alert((currentLang === 'zh' ? '删除失败: ' : 'Delete failed: ') + dd.message); - overlay.remove(); - } - } catch (err) { - alert((currentLang === 'zh' ? '错误: ' : 'Error: ') + err); - overlay.remove(); - } - }; - - btnRow.appendChild(cancelBtn); - btnRow.appendChild(confirmBtn); - overlay.appendChild(msg); - overlay.appendChild(btnRow); - - card.appendChild(overlay); - }; - - card.appendChild(img); - card.appendChild(delBtn); - this.galleryGrid.insertBefore(card, this.gallerySentinel); - }); - - this.galleryCurrentPage = page; - this.galleryHasMore = page < data.pages; - - if (!this.galleryHasMore) { - this.gallerySentinel.innerHTML = '没有更多图片了 / No more images'; - } else { - this.gallerySentinel.innerHTML = '向下滚动加载更多 / Scroll for more'; - } - } else { - this.galleryHasMore = false; - this.gallerySentinel.innerHTML = reset ? '图库为空 / Gallery is empty' : '没有更多图片了 / No more images'; - } - } catch (e) { - console.error('Failed to load gallery images', e); - this.gallerySentinel.innerHTML = '加载失败 / Load failed'; - } - - this.galleryLoading = false; - } - - - showEditModal(model) { - const modal = document.createElement('div'); - modal.style.position = 'fixed'; - modal.style.top = '0'; - modal.style.left = '0'; - modal.style.width = '100vw'; - modal.style.height = '100vh'; - modal.style.background = 'rgba(0,0,0,0.6)'; - modal.style.backdropFilter = 'blur(4px)'; - modal.style.zIndex = '10000'; - modal.style.display = 'flex'; - modal.style.alignItems = 'center'; - modal.style.justifyContent = 'center'; - - const content = document.createElement('div'); - content.style.background = '#202124'; - content.style.padding = '24px'; - content.style.borderRadius = '12px'; - content.style.width = '720px'; - content.style.maxWidth = '90%'; - content.style.border = '1px solid #3c4043'; - content.style.boxShadow = '0 8px 32px rgba(0,0,0,0.5)'; - content.style.display = 'flex'; - content.style.flexDirection = 'row'; - content.style.gap = '24px'; - content.style.fontFamily = 'Inter, Roboto, sans-serif'; - - // --- LEFT COLUMN --- - const leftCol = document.createElement('div'); - leftCol.style.width = '240px'; - leftCol.style.flexShrink = '0'; - leftCol.style.display = 'flex'; - leftCol.style.flexDirection = 'column'; - leftCol.style.gap = '12px'; - - const previewContainer = document.createElement('div'); - previewContainer.style.width = '100%'; - previewContainer.style.height = '320px'; - previewContainer.style.background = '#303134'; - previewContainer.style.borderRadius = '8px'; - previewContainer.style.display = 'flex'; - previewContainer.style.alignItems = 'center'; - previewContainer.style.justifyContent = 'center'; - previewContainer.style.overflow = 'hidden'; - previewContainer.style.border = '1px solid #3c4043'; - previewContainer.style.position = 'relative'; - - if (model.preview_url) { - const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i); - if (isVideo) { - const video = document.createElement('video'); - video.src = model.preview_url; - video.controls = false; - video.autoplay = true; - video.loop = true; - video.muted = true; - video.style.width = '100%'; - video.style.height = '100%'; - video.style.objectFit = 'cover'; - - const muteBtn = document.createElement('div'); - muteBtn.innerHTML = '🔇'; - muteBtn.style.position = 'absolute'; - muteBtn.style.bottom = '8px'; - muteBtn.style.right = '8px'; - muteBtn.style.background = 'rgba(0,0,0,0.6)'; - muteBtn.style.color = '#fff'; - muteBtn.style.padding = '6px'; - muteBtn.style.borderRadius = '50%'; - muteBtn.style.cursor = 'pointer'; - muteBtn.style.fontSize = '14px'; - muteBtn.style.zIndex = '10'; - muteBtn.title = window.anomalous_browser_lang === 'zh' ? '开启/关闭声音' : 'Toggle Sound'; - muteBtn.onclick = (e) => { - e.stopPropagation(); - video.muted = !video.muted; - muteBtn.innerHTML = video.muted ? '🔇' : '🔊'; - }; - - previewContainer.appendChild(video); - previewContainer.appendChild(muteBtn); - } else { - const img = document.createElement('img'); - img.src = model.preview_url; - img.style.width = '100%'; - img.style.height = '100%'; - img.style.objectFit = 'cover'; - previewContainer.appendChild(img); - } - } else { - previewContainer.innerHTML = `
${window.anomalous_browser_lang === 'zh' ? '暂无封面' : 'No Cover'}
`; - } - - const coverRow = document.createElement('div'); - coverRow.style.display = 'flex'; - coverRow.style.flexDirection = 'column'; - coverRow.style.gap = '8px'; - - const galleryBtn = document.createElement('button'); - galleryBtn.innerHTML = '🖼️ ' + (window.anomalous_browser_lang === 'zh' ? '从历史图库挑选' : 'Pick from Gallery'); - galleryBtn.style.padding = '8px'; - galleryBtn.style.background = '#303134'; - galleryBtn.style.color = '#8ab4f8'; - galleryBtn.style.border = '1px solid #5f6368'; - galleryBtn.style.borderRadius = '6px'; - galleryBtn.style.cursor = 'pointer'; - galleryBtn.style.fontWeight = '500'; - galleryBtn.style.fontSize = '0.9em'; - galleryBtn.onmouseover = () => galleryBtn.style.background = '#3c4043'; - galleryBtn.onmouseout = () => galleryBtn.style.background = '#303134'; - galleryBtn.onclick = () => { - document.body.removeChild(modal); - this.showGallerySelectMode(model); - }; - - const localBtn = document.createElement('button'); - localBtn.innerHTML = '📁 ' + (window.anomalous_browser_lang === 'zh' ? '从本地上传' : 'Upload Local'); - localBtn.style.padding = '8px'; - localBtn.style.background = '#303134'; - localBtn.style.color = '#8ab4f8'; - localBtn.style.border = '1px solid #5f6368'; - localBtn.style.borderRadius = '6px'; - localBtn.style.cursor = 'pointer'; - localBtn.style.fontWeight = '500'; - localBtn.style.fontSize = '0.9em'; - localBtn.onmouseover = () => localBtn.style.background = '#3c4043'; - localBtn.onmouseout = () => localBtn.style.background = '#303134'; - localBtn.onclick = () => { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/*'; - fileInput.onchange = async (e) => { - if (e.target.files && e.target.files.length > 0) { - const file = e.target.files[0]; - const formData = new FormData(); - formData.append('type', this.currentType); - formData.append('path_idx', this.currentPathIdx); - formData.append('subfolder', this.currentSubfolder); - formData.append('filename', model.filename); - formData.append('image', file); - try { - const res = await fetch('/anomalous/upload_custom_cover', { method: 'POST', body: formData }); - const data = await res.json(); - if (data.status === 'success') { - await this.loadModels(); - const updatedModel = this.models.find(m => m.filename === model.filename); - if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) { - if (updatedModel) this.showDetail(updatedModel); - } - document.body.removeChild(modal); - } else { - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); - } - } catch (err) { - alert((window.anomalous_browser_lang === 'zh' ? '上传失败: ' : 'Upload failed: ') + err); - } - } - }; - fileInput.click(); - }; - - coverRow.appendChild(galleryBtn); - coverRow.appendChild(localBtn); - - leftCol.appendChild(previewContainer); - leftCol.appendChild(coverRow); - - // --- RIGHT COLUMN --- - const rightCol = document.createElement('div'); - rightCol.style.flex = '1'; - rightCol.style.display = 'flex'; - rightCol.style.flexDirection = 'column'; - rightCol.style.gap = '15px'; - - const title = document.createElement('h2'); - title.innerText = window.anomalous_browser_lang === 'zh' ? '编辑模型信息' : 'Edit Model Info'; - title.style.margin = '0'; - title.style.color = '#e8eaed'; - title.style.fontSize = '1.25em'; - title.style.fontWeight = '500'; - - const filenameLabel = document.createElement('div'); - filenameLabel.innerHTML = `${window.anomalous_browser_lang === 'zh' ? '文件:' : 'File:'} ${model.filename}`; - filenameLabel.style.color = '#e8eaed'; - filenameLabel.style.fontSize = '0.9em'; - filenameLabel.style.wordBreak = 'break-all'; - - const inputStyle = ` - width: 100%; - padding: 12px 14px; - background: #303134; - color: #e8eaed; - border: 1px solid #5f6368; - border-radius: 6px; - box-sizing: border-box; - outline: none; - font-size: 14px; - transition: border 0.2s; - `; - - const nameInput = document.createElement('input'); - nameInput.placeholder = window.anomalous_browser_lang === 'zh' ? '自定义名称 (留空则使用原名)' : 'Custom Name (Leave empty to use original)'; - nameInput.value = (model.metadata && model.metadata.custom_name) ? model.metadata.custom_name : ''; - nameInput.style.cssText = inputStyle; - nameInput.onfocus = () => nameInput.style.borderColor = '#8ab4f8'; - nameInput.onblur = () => nameInput.style.borderColor = '#5f6368'; - - const notesInput = document.createElement('textarea'); - notesInput.placeholder = window.anomalous_browser_lang === 'zh' ? '📓 专属备注... (支持多行)' : '📓 Custom Notes...'; - notesInput.value = (model.metadata && model.metadata.custom_notes) ? model.metadata.custom_notes : ''; - notesInput.style.cssText = inputStyle; - notesInput.style.flex = '1'; // fill remaining space - notesInput.style.minHeight = '150px'; - notesInput.style.resize = 'vertical'; - // Notebook styling override - notesInput.style.background = 'linear-gradient(135deg, #262522 0%, #202124 100%)'; - notesInput.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)'; - notesInput.style.backgroundAttachment = 'local'; - notesInput.style.border = '1px solid #3c4043'; - notesInput.style.borderLeft = '4px solid #a38d53'; - notesInput.style.borderRadius = '4px 8px 8px 4px'; - notesInput.style.color = '#d1c9b4'; - notesInput.style.fontFamily = '"Consolas", "Courier New", monospace'; - notesInput.style.lineHeight = '24px'; - // Removed text shadow for cleaner look - - notesInput.onfocus = () => { - notesInput.style.boxShadow = '0 0 0 2px rgba(163, 141, 83, 0.2)'; - notesInput.style.borderColor = '#a38d53'; - }; - notesInput.onblur = () => { - notesInput.style.boxShadow = 'none'; - notesInput.style.borderColor = '#3c4043'; - }; - - const physicalRow = document.createElement('div'); - physicalRow.style.display = 'flex'; - physicalRow.style.flexDirection = 'column'; - physicalRow.style.gap = '4px'; - - const physicalCheckboxWrapper = document.createElement('div'); - physicalCheckboxWrapper.style.display = 'flex'; - physicalCheckboxWrapper.style.alignItems = 'center'; - physicalCheckboxWrapper.style.gap = '8px'; - - const physicalCheckbox = document.createElement('input'); - physicalCheckbox.type = 'checkbox'; - physicalCheckbox.id = 'anomalous-physical-rename-checkbox'; - physicalCheckbox.style.cursor = 'pointer'; - - const physicalLabel = document.createElement('label'); - physicalLabel.htmlFor = 'anomalous-physical-rename-checkbox'; - physicalLabel.innerText = window.anomalous_browser_lang === 'zh' ? '同步物理重命名' : 'Physical Rename on Disk'; - physicalLabel.style.color = '#e8eaed'; - physicalLabel.style.fontSize = '0.9em'; - physicalLabel.style.cursor = 'pointer'; - - physicalCheckboxWrapper.appendChild(physicalCheckbox); - physicalCheckboxWrapper.appendChild(physicalLabel); - - const physicalDesc = document.createElement('div'); - physicalDesc.style.fontSize = '0.8em'; - physicalDesc.style.color = '#9aa0a6'; - physicalDesc.style.marginLeft = '22px'; - physicalDesc.innerText = window.anomalous_browser_lang === 'zh' ? '若不勾选则仅虚拟重命名,安全不破坏原文件。' : 'Virtual rename only if unchecked. Safe for originals.'; - - physicalRow.appendChild(physicalCheckboxWrapper); - physicalRow.appendChild(physicalDesc); - - const actionRow = document.createElement('div'); - actionRow.style.display = 'flex'; - actionRow.style.justifyContent = 'space-between'; - actionRow.style.marginTop = 'auto'; - - const leftActions = document.createElement('div'); - leftActions.style.display = 'flex'; - leftActions.style.gap = '10px'; - - const resetBtn = document.createElement('button'); - resetBtn.innerText = window.anomalous_browser_lang === 'zh' ? '重置设置' : 'Reset All'; - resetBtn.style.padding = '8px 16px'; - resetBtn.style.background = 'transparent'; - resetBtn.style.color = '#f28b82'; - resetBtn.style.border = '1px solid #f28b82'; - resetBtn.style.borderRadius = '4px'; - resetBtn.style.cursor = 'pointer'; - resetBtn.style.fontWeight = '500'; - resetBtn.onclick = async () => { - if (!confirm(window.anomalous_browser_lang === 'zh' ? '确定要重置该模型的所有自定义信息(名称、封面、备注)吗?' : 'Reset all custom info for this model?')) return; - document.body.removeChild(modal); - try { - const res = await fetch('/anomalous/update_metadata', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: this.currentType, - path_idx: this.currentPathIdx, - subfolder: this.currentSubfolder, - filename: model.filename, - custom_name: '', - custom_notes: '', - reset_cover: true, - physical_rename: false - }) - }); - if (res.ok) { - await this.loadModels(); - const updatedModel = this.models.find(m => m.filename === model.filename); - if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) { - if (updatedModel) { - this.showDetail(updatedModel); - } else { - this.grid.style.display = 'grid'; - this.detailPanel.style.display = 'none'; - } - } - } - } catch (e) { console.error(e); } - }; - - const cancelBtn = document.createElement('button'); - cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel'; - cancelBtn.style.padding = '8px 16px'; - cancelBtn.style.background = 'transparent'; - cancelBtn.style.color = '#8ab4f8'; - cancelBtn.style.border = 'none'; - cancelBtn.style.borderRadius = '4px'; - cancelBtn.style.cursor = 'pointer'; - cancelBtn.style.fontWeight = '500'; - cancelBtn.onclick = () => document.body.removeChild(modal); - - leftActions.appendChild(resetBtn); - leftActions.appendChild(cancelBtn); - - const saveBtn = document.createElement('button'); - saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存更改' : 'Save Changes'; - saveBtn.style.padding = '8px 24px'; - saveBtn.style.background = '#8ab4f8'; - saveBtn.style.color = '#202124'; - saveBtn.style.border = 'none'; - saveBtn.style.borderRadius = '4px'; - saveBtn.style.cursor = 'pointer'; - saveBtn.style.fontWeight = '600'; - saveBtn.onclick = () => { - const newName = nameInput.value.trim(); - const newNotes = notesInput.value.trim(); - saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存中...' : 'Saving...'; - saveBtn.disabled = true; - - fetch('/anomalous/update_metadata', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: this.currentType, - path_idx: this.currentPathIdx, - subfolder: this.currentSubfolder, - filename: model.filename, - custom_name: newName, - custom_notes: newNotes, - physical_rename: physicalCheckbox.checked - }) - }).then(res => res.json()).then(data => { - document.body.removeChild(modal); - if (data.status === 'success') { - if (!model.metadata) model.metadata = {}; - model.metadata.custom_name = newName; - model.metadata.custom_notes = newNotes; - if (data.new_filename && physicalCheckbox.checked) { - model.filename = data.new_filename; - } - this.loadModels(); - if (this.currentDetailModel && (this.currentDetailModel.filename === model.filename || (data.new_filename && this.currentDetailModel.filename === data.new_filename))) { - this.showDetail(model); - } - } else { - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message); - } - }).catch(e => { - document.body.removeChild(modal); - alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + e); - }); - }; - - actionRow.appendChild(leftActions); - actionRow.appendChild(saveBtn); - - rightCol.appendChild(title); - rightCol.appendChild(filenameLabel); - rightCol.appendChild(nameInput); - rightCol.appendChild(notesInput); - rightCol.appendChild(physicalRow); - rightCol.appendChild(actionRow); - - content.appendChild(leftCol); - content.appendChild(rightCol); - modal.appendChild(content); - document.body.appendChild(modal); - } - - - async showGeneratedGallery(model) { - let overlay = document.getElementById('anomalous-generated-gallery-overlay'); - if (!overlay) { - overlay = document.createElement('div'); - overlay.id = 'anomalous-generated-gallery-overlay'; - overlay.style.position = 'fixed'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100vw'; - overlay.style.height = '100vh'; - overlay.style.backgroundColor = 'var(--comfy-menu-bg, #222)'; - overlay.style.zIndex = '9990'; - overlay.style.display = 'flex'; - overlay.style.flexDirection = 'column'; - overlay.style.overflow = 'hidden'; - - const header = document.createElement('div'); - header.style.padding = '15px 25px'; - header.style.background = '#333'; - header.style.display = 'flex'; - header.style.justifyContent = 'space-between'; - header.style.alignItems = 'center'; - header.style.borderBottom = '1px solid #444'; - - const title = document.createElement('h2'); - title.id = 'anomalous-generated-gallery-title'; - title.style.margin = '0'; - title.style.color = '#fff'; - - const closeBtn = document.createElement('button'); - closeBtn.innerHTML = '✖ ' + (window.anomalous_browser_lang === 'zh' ? '关闭图库' : 'Close Gallery'); - closeBtn.style.padding = '8px 15px'; - closeBtn.style.background = '#dc3545'; - closeBtn.style.color = '#fff'; - closeBtn.style.border = 'none'; - closeBtn.style.borderRadius = '5px'; - closeBtn.style.cursor = 'pointer'; - closeBtn.style.fontWeight = 'bold'; - closeBtn.onmouseover = () => closeBtn.style.background = '#c82333'; - closeBtn.onmouseout = () => closeBtn.style.background = '#dc3545'; - closeBtn.onclick = () => { - overlay.style.display = 'none'; - }; - - header.appendChild(title); - header.appendChild(closeBtn); - overlay.appendChild(header); - - const contentCont = document.createElement('div'); - contentCont.id = 'anomalous-generated-gallery-content'; - contentCont.style.flex = '1'; - contentCont.style.overflowY = 'auto'; - contentCont.style.padding = '20px'; - contentCont.style.display = 'grid'; - contentCont.style.gridTemplateColumns = 'repeat(auto-fill, minmax(200px, 1fr))'; - contentCont.style.gap = '15px'; - contentCont.style.alignContent = 'start'; - overlay.appendChild(contentCont); - - document.body.appendChild(overlay); - } - - const title = document.getElementById('anomalous-generated-gallery-title'); - title.innerText = (window.anomalous_browser_lang === 'zh' ? '历史生成图库: ' : 'Generated History: ') + model.name; - - const contentCont = document.getElementById('anomalous-generated-gallery-content'); - contentCont.innerHTML = ''; - - const loading = document.createElement('div'); - loading.innerText = window.anomalous_browser_lang === 'zh' ? '加载中,正在扫描图片元数据...' : 'Loading, scanning metadata...'; - loading.style.textAlign = 'center'; - loading.style.gridColumn = '1 / -1'; - loading.style.padding = '50px'; - loading.style.color = '#aaa'; - contentCont.appendChild(loading); - - overlay.style.display = 'flex'; - - try { - const res = await fetch('/anomalous/model_images?model_name=' + encodeURIComponent(model.path)); - const data = await res.json(); - contentCont.innerHTML = ''; - - if (!data.images || data.images.length === 0) { - const emptyMsg = document.createElement('div'); - emptyMsg.innerText = window.anomalous_browser_lang === 'zh' ? '没有找到使用此模型生成的历史图片。' : 'No images found generated by this model.'; - emptyMsg.style.textAlign = 'center'; - emptyMsg.style.gridColumn = '1 / -1'; - emptyMsg.style.padding = '50px'; - emptyMsg.style.color = '#888'; - contentCont.appendChild(emptyMsg); - return; - } - - data.images.forEach(img => { - const imgCont = document.createElement('div'); - imgCont.style.position = 'relative'; - imgCont.style.cursor = 'pointer'; - imgCont.style.borderRadius = '8px'; - imgCont.style.overflow = 'hidden'; - imgCont.style.boxShadow = '0 4px 6px rgba(0,0,0,0.3)'; - imgCont.style.aspectRatio = '1 / 1'; - imgCont.style.background = '#000'; - - const el = document.createElement('img'); - el.src = img.url; - el.style.width = '100%'; - el.style.height = '100%'; - el.style.objectFit = 'contain'; - el.style.transition = 'transform 0.2s'; - - imgCont.onmouseover = () => { el.style.transform = 'scale(1.05)'; }; - imgCont.onmouseout = () => { el.style.transform = 'scale(1)'; }; - imgCont.onclick = () => { - this.showGalleryViewer(img.url); - }; - - imgCont.appendChild(el); - contentCont.appendChild(imgCont); - }); - } catch (e) { - contentCont.innerHTML = '
Error loading images
'; - } - } - - showGallerySelectMode(model) { - this.gallerySelectModel = model; - this.grid.style.display = 'none'; - this.detailPanel.style.display = 'none'; - this.galleryPanel.style.display = 'flex'; - let banner = document.getElementById('anomalous-gallery-select-banner'); - if (!banner) { - banner = document.createElement('div'); - banner.id = 'anomalous-gallery-select-banner'; - banner.style.background = '#28a745'; - banner.style.color = '#fff'; - banner.style.padding = '10px'; - banner.style.textAlign = 'center'; - banner.style.fontWeight = 'bold'; - banner.style.position = 'sticky'; - banner.style.top = '0'; - banner.style.zIndex = '1000'; - this.galleryPanel.insertBefore(banner, this.galleryPanel.firstChild); - } - banner.style.display = 'block'; - banner.innerHTML = window.anomalous_browser_lang === 'zh' - ? `正在为模型 ${model.filename} 选择封面。请点击下方的图片。` - : `Selecting cover for ${model.filename}. Click an image below.`; - - document.getElementById('anomalous-cancel-select').onclick = () => { - const tempModel = this.gallerySelectModel; - this.gallerySelectModel = null; - banner.style.display = 'none'; - this.galleryPanel.style.display = 'none'; - if (this.currentDetailModel) { - this.detailPanel.style.display = 'flex'; - } else { - this.grid.style.display = 'grid'; - } - if (tempModel) { - this.showEditModal(tempModel); - } - }; - - if (this.galleryImages.length === 0) { - this.loadGalleryImages(1, true); - } - } - - showGalleryViewer(src) { - let viewer = document.getElementById('anomalous-gallery-viewer'); - if (!viewer) { - viewer = document.createElement('div'); - viewer.id = 'anomalous-gallery-viewer'; - viewer.className = 'anomalous-gallery-viewer'; - - const closeBtn = document.createElement('div'); - closeBtn.className = 'anomalous-gallery-viewer-close'; - closeBtn.innerHTML = '×'; - - const img = document.createElement('img'); - img.id = 'anomalous-gallery-viewer-img'; - - viewer.appendChild(img); - viewer.appendChild(closeBtn); - - let scale = 1; - let translateX = 0; - let translateY = 0; - let isDragging = false; - let startX = 0, startY = 0; - - const resetImgTransform = () => { - scale = 1; translateX = 0; translateY = 0; - img.style.transform = `translate(0px, 0px) scale(1)`; - img.style.cursor = 'grab'; - }; - - closeBtn.onclick = () => { - viewer.style.display = 'none'; - resetImgTransform(); - }; - - viewer.onclick = (e) => { - if (e.target === viewer) { - viewer.style.display = 'none'; - resetImgTransform(); - } - }; - - viewer.addEventListener('wheel', (e) => { - e.preventDefault(); - const zoomFactor = 0.1; - if (e.deltaY < 0) scale += zoomFactor; - else scale = Math.max(0.1, scale - zoomFactor); - img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`; - }); - - img.addEventListener('mousedown', (e) => { - e.preventDefault(); - isDragging = true; - startX = e.clientX - translateX; - startY = e.clientY - translateY; - img.style.cursor = 'grabbing'; - }); - - window.addEventListener('mousemove', (e) => { - if (!isDragging) return; - translateX = e.clientX - startX; - translateY = e.clientY - startY; - img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`; - }); - - window.addEventListener('mouseup', () => { - isDragging = false; - img.style.cursor = 'grab'; - }); - - document.body.appendChild(viewer); - } - - const img = document.getElementById('anomalous-gallery-viewer-img'); - img.src = src; - - // Reset scale and translation when opening a new image - img.style.transform = `translate(0px, 0px) scale(1)`; - img.style.cursor = 'grab'; - - viewer.style.display = 'flex'; - } - - async refreshNotebooks(autoOpenFirst = false) { - try { - const res = await fetch('/anomalous/notebooks'); - const data = await res.json(); - this.nbListEl.innerHTML = ''; - - if (data.notebooks && data.notebooks.length > 0) { - if (autoOpenFirst) { - if (!this.currentNotebook) { - this.currentNotebook = data.notebooks[0]; - } - if (this.currentNotebook) { - this.renderNotebookEditor(); - } - } - - data.notebooks.forEach(nb => { - const item = document.createElement('div'); - item.className = 'anomalous-nb-item'; - if (this.currentNotebook && this.currentNotebook.filename === nb.filename) { - item.classList.add('active'); - } - item.innerHTML = `📄 ${nb.name}`; - item.onclick = () => { - this.currentNotebook = nb; - this.renderNotebookEditor(); - this.refreshNotebooks(); - }; - this.nbListEl.appendChild(item); - }); - } - } catch (e) { } - } - - async saveCurrentNotebook() { - if (!this.currentNotebook) return; - try { - await fetch('/anomalous/save_notebook', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(this.currentNotebook) - }); - this.refreshNotebooks(); - } catch (e) { } - } - - async deleteCurrentNotebook(skipConfirm = false) { - if (!this.currentNotebook) return; - if (!skipConfirm && !confirm(t('deleteNotebook') + ' ?')) return; - try { - await fetch('/anomalous/delete_notebook', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename: this.currentNotebook.filename }) - }); - this.currentNotebook = null; - this.nbEditor.innerHTML = ''; - this.refreshNotebooks(); - } catch (e) { } - } - - renderNotebookEditor() { - this.nbEditor.innerHTML = ''; - if (!this.currentNotebook) return; - - const data = this.currentNotebook.data || {}; - if (!data.loras) data.loras = []; - - // Toolbar - const tb = document.createElement('div'); - tb.className = 'anomalous-nb-toolbar'; - - const titleArea = document.createElement('h3'); - titleArea.innerHTML = this.currentNotebook.name; - titleArea.style.margin = '0'; - - const rightBtns = document.createElement('div'); - - const saveBtn = document.createElement('button'); - saveBtn.innerHTML = t('saveNotebook'); - saveBtn.className = 'anomalous-btn-primary'; - saveBtn.onclick = async () => { - const orig = saveBtn.innerHTML; - saveBtn.innerHTML = '⏳...'; - await this.saveCurrentNotebook(); - saveBtn.innerHTML = '✅'; - saveBtn.style.background = '#2e8b57'; - setTimeout(() => { - saveBtn.innerHTML = orig; - saveBtn.style.background = ''; - }, 1500); - }; - - let delTimer = null; - const delContainer = document.createElement('span'); - delContainer.style.display = 'inline-flex'; - delContainer.style.alignItems = 'center'; - - const delBtn = document.createElement('button'); - delBtn.innerHTML = t('deleteNotebook'); - delBtn.className = 'anomalous-btn-danger'; - - const cancelDelBtn = document.createElement('button'); - cancelDelBtn.innerHTML = '✕'; - cancelDelBtn.className = 'anomalous-btn-danger'; - cancelDelBtn.style.display = 'none'; - cancelDelBtn.style.background = '#555'; - cancelDelBtn.style.marginLeft = '2px'; - cancelDelBtn.style.padding = '6px 8px'; - - delContainer.appendChild(delBtn); - delContainer.appendChild(cancelDelBtn); - - const resetDel = () => { - clearTimeout(delTimer); - delBtn.innerHTML = t('deleteNotebook'); - delBtn.style.background = ''; - cancelDelBtn.style.display = 'none'; - }; - - delBtn.onclick = () => { - if (delBtn.innerHTML === t('deleteNotebook')) { - delBtn.innerHTML = t('delSure'); - delBtn.style.background = '#800'; - cancelDelBtn.style.display = 'block'; - delTimer = setTimeout(resetDel, 4000); - } else { - resetDel(); - this.deleteCurrentNotebook(true); - } - }; - - cancelDelBtn.onclick = resetDel; - - const sendBtn = document.createElement('button'); - sendBtn.innerHTML = t('sendToCanvas'); - sendBtn.className = 'anomalous-btn-success'; - sendBtn.onclick = () => this.sendNotebookToCanvas(); - - rightBtns.appendChild(saveBtn); - rightBtns.appendChild(sendBtn); - rightBtns.appendChild(delContainer); - - tb.appendChild(titleArea); - tb.appendChild(rightBtns); - - // Settings / Models - const modelSection = document.createElement('div'); - modelSection.className = 'anomalous-nb-section'; - - // Base Model - const baseRow = document.createElement('div'); - baseRow.className = 'anomalous-nb-row'; - baseRow.innerHTML = `${t('baseModel')}`; - const baseSelect = document.createElement('select'); - baseSelect.className = 'anomalous-nb-select'; - const buildSelect = (bases) => { - baseSelect.innerHTML = ''; - bases.forEach(b => { - const opt = document.createElement('option'); - opt.value = b; opt.text = b; - if (data.baseModel === b) opt.selected = true; - baseSelect.appendChild(opt); - }); - if (!data.baseModel && bases.length > 0) data.baseModel = bases[0]; - }; - - if (this.baseModelsCache) { - buildSelect(this.baseModelsCache); - } else { - const tempBases = ['SD 1.5', 'SD 2.1', 'SDXL', 'SD 3.0', 'SD 3.5', 'Flux.1', 'Pony', 'HunyuanVideo', 'LTX-Video', 'OmniGen']; - buildSelect(tempBases); - fetch('/anomalous/base_models').then(r => r.json()).then(d => { - if (d.base_models && d.base_models.length > 0) { - this.baseModelsCache = d.base_models; - buildSelect(this.baseModelsCache); - } - }).catch(e => { }); - } - if (!data.baseModel) data.baseModel = 'SDXL'; - baseSelect.onchange = () => { - data.baseModel = baseSelect.value; - data.mainModel = null; - data.loras = []; - this.saveCurrentNotebook(); - this.renderNotebookEditor(); - }; - baseRow.appendChild(baseSelect); - - // Main Model (Card Selection) - const mainBox = document.createElement('div'); - mainBox.className = 'anomalous-nb-gallery-box'; - const mainRow = document.createElement('div'); - mainRow.className = 'anomalous-nb-row'; - mainRow.innerHTML = `${t('mainModel')}`; - - const mainGallery = document.createElement('div'); - mainGallery.className = 'anomalous-nb-gallery-wrap'; - - mainBox.appendChild(mainRow); - mainBox.appendChild(mainGallery); - - // Loras (Card Selection) - const loraBox = document.createElement('div'); - loraBox.className = 'anomalous-nb-gallery-box'; - const loraRow = document.createElement('div'); - loraRow.className = 'anomalous-nb-row'; - loraRow.innerHTML = `Loras`; - - const loraGallery = document.createElement('div'); - loraGallery.className = 'anomalous-nb-gallery-wrap'; - - loraBox.appendChild(loraRow); - loraBox.appendChild(loraGallery); - - modelSection.appendChild(baseRow); - modelSection.appendChild(mainBox); - modelSection.appendChild(loraBox); - - // Prompt Section - const promptSec = document.createElement('div'); - promptSec.className = 'anomalous-nb-section'; - - // Toolbar - const pToolbar = document.createElement('div'); - pToolbar.className = 'anomalous-nb-prompt-toolbar'; - - const langSelect = document.createElement('select'); - langSelect.className = 'anomalous-nb-select'; - const langs = [ - { v: 'zh-CN', l: '🇨🇳 中文 (zh-CN)' }, { v: 'en', l: '🇬🇧 English (en)' }, - { v: 'ja', l: '🇯🇵 日本语 (ja)' }, { v: 'ko', l: '🇰🇷 한국어 (ko)' }, - { v: 'fr', l: '🇫🇷 Français (fr)' }, { v: 'de', l: '🇩🇪 Deutsch (de)' }, - { v: 'es', l: '🇪🇸 Español (es)' }, { v: 'ru', l: '🇷🇺 Русский (ru)' } - ]; - langs.forEach(lg => { - const opt = document.createElement('option'); - opt.value = lg.v; opt.text = lg.l; - if ((data.targetLang || 'zh-CN') === lg.v) opt.selected = true; - langSelect.appendChild(opt); - }); - langSelect.onchange = () => { - data.targetLang = langSelect.value; - data.translations = {}; // Clear translation cache on lang change - this.saveCurrentNotebook(); - updateVisualTags(); - }; - - const findInput = document.createElement('input'); - findInput.className = 'anomalous-nb-select'; - findInput.placeholder = t('findPlaceholder'); - findInput.style.flex = '1'; - - const replaceInput = document.createElement('input'); - replaceInput.className = 'anomalous-nb-select'; - replaceInput.placeholder = t('replacePlaceholder'); - replaceInput.style.flex = '1'; - - const replaceBtn = document.createElement('button'); - replaceBtn.className = 'anomalous-btn-primary'; - replaceBtn.innerHTML = t('replaceAll'); - - pToolbar.appendChild(langSelect); - pToolbar.appendChild(findInput); - pToolbar.appendChild(replaceInput); - pToolbar.appendChild(replaceBtn); - - // Raw Input Toggle - const toggleRow = document.createElement('div'); - toggleRow.style.display = 'flex'; - toggleRow.style.justifyContent = 'space-between'; - toggleRow.style.marginBottom = '5px'; - toggleRow.innerHTML = `Prompt Editor`; - const rawBtn = document.createElement('button'); - rawBtn.className = 'anomalous-btn-primary'; - rawBtn.innerHTML = t('editRaw'); - toggleRow.appendChild(rawBtn); - - // Raw Textarea - const rawArea = document.createElement('textarea'); - rawArea.className = 'anomalous-nb-textarea'; - rawArea.value = data.promptEn || ''; - rawArea.style.display = 'none'; - rawArea.style.height = '150px'; - - // Visual Dual Pane - const dualPane = document.createElement('div'); - dualPane.className = 'anomalous-nb-dual-pane'; - - if (!data.translations) data.translations = {}; - - const updateVisualTags = () => { - dualPane.innerHTML = ''; - const txt = rawArea.value; - data.promptEn = txt; - this.saveCurrentNotebook(); - if (!txt.trim()) return; - - const tags = txt.split(',').map(s => s.trim()).filter(s => s); - tags.forEach((tag, idx) => { - const tagRow = document.createElement('div'); - tagRow.className = 'anomalous-nb-tag-row'; - - const tagL = document.createElement('div'); - tagL.className = 'anomalous-nb-visual-tag'; - tagL.style.flex = '1'; - tagL.style.justifyContent = 'space-between'; - const txtL = document.createElement('span'); - txtL.innerText = tag; - const copyL = document.createElement('span'); - copyL.className = 'anomalous-nb-copy-btn'; - copyL.innerHTML = '📋'; - copyL.onclick = (e) => { - e.stopPropagation(); - navigator.clipboard.writeText(tag).then(() => { copyL.innerHTML = '✅'; setTimeout(() => copyL.innerHTML = '📋', 1000); }); - }; - tagL.appendChild(txtL); - tagL.appendChild(copyL); - - const tagR = document.createElement('div'); - tagR.className = 'anomalous-nb-visual-tag'; - tagR.style.flex = '1'; - tagR.style.justifyContent = 'space-between'; - const transTxt = data.translations[tag] ? data.translations[tag] : '...'; - const txtR = document.createElement('span'); - txtR.innerText = transTxt; - const copyR = document.createElement('span'); - copyR.className = 'anomalous-nb-copy-btn'; - copyR.innerHTML = '📋'; - copyR.onclick = (e) => { - e.stopPropagation(); - navigator.clipboard.writeText(txtR.innerText).then(() => { copyR.innerHTML = '✅'; setTimeout(() => copyR.innerHTML = '📋', 1000); }); - }; - tagR.appendChild(txtR); - tagR.appendChild(copyR); - - tagL.onmouseenter = () => { tagL.classList.add('hover'); tagR.classList.add('hover'); }; - tagL.onmouseleave = () => { tagL.classList.remove('hover'); tagR.classList.remove('hover'); }; - tagR.onmouseenter = () => { tagL.classList.add('hover'); tagR.classList.add('hover'); }; - tagR.onmouseleave = () => { tagL.classList.remove('hover'); tagR.classList.remove('hover'); }; - - tagL.onclick = () => { - const inp = document.createElement('input'); - inp.value = tag; inp.className = 'anomalous-nb-tag-edit'; - tagL.innerHTML = ''; tagL.appendChild(inp); inp.focus(); - const finish = () => { - tags[idx] = inp.value.trim(); - rawArea.value = tags.join(', '); - updateVisualTags(); - }; - inp.onblur = finish; - inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); }; - }; - - tagR.onclick = () => { - const inp = document.createElement('input'); - inp.value = data.translations[tag] || ''; inp.className = 'anomalous-nb-tag-edit'; - tagR.innerHTML = ''; tagR.appendChild(inp); inp.focus(); - const finish = () => { - data.translations[tag] = inp.value.trim(); - this.saveCurrentNotebook(); - updateVisualTags(); - }; - inp.onblur = finish; - inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); }; - }; - - tagRow.appendChild(tagL); - tagRow.appendChild(tagR); - dualPane.appendChild(tagRow); - - if (!data.translations[tag]) { - fetch('/anomalous/translate', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: tag, target_lang: data.targetLang || 'zh-CN' }) - }).then(r => r.json()).then(d => { - if (d.translated) { - data.translations[tag] = d.translated; - txtR.innerText = d.translated; - this.saveCurrentNotebook(); - } - }).catch(() => { }); - } - }); - }; - - rawBtn.onclick = () => { - if (rawArea.style.display === 'none') { - rawArea.style.display = 'block'; - dualPane.style.display = 'none'; - rawBtn.innerHTML = '👁️ Done Editing'; - pToolbar.style.display = 'none'; - } else { - rawArea.style.display = 'none'; - dualPane.style.display = 'flex'; - rawBtn.innerHTML = '📝 Edit Raw / Paste'; - pToolbar.style.display = 'flex'; - updateVisualTags(); - } - }; - - replaceBtn.onclick = () => { - const findStr = findInput.value; - const repStr = replaceInput.value; - if (!findStr) return; - const newTxt = rawArea.value.split(findStr).join(repStr); - rawArea.value = newTxt; - updateVisualTags(); - }; - - rawArea.oninput = () => { - clearTimeout(this.pTimeout); - this.pTimeout = setTimeout(() => { data.promptEn = rawArea.value; this.saveCurrentNotebook(); }, 500); - }; - - updateVisualTags(); - - promptSec.appendChild(pToolbar); - promptSec.appendChild(toggleRow); - promptSec.appendChild(rawArea); - promptSec.appendChild(dualPane); - - this.nbEditor.appendChild(tb); - this.nbEditor.appendChild(modelSection); - this.nbEditor.appendChild(promptSec); - - // Fetch compatible models and fill galleries - this.fillNotebookGalleries(data.baseModel, mainGallery, loraGallery, data); - } - - fillNotebookGalleries(baseModel, mainGallery, loraGallery, data) { - if (!baseModel) return; - - const buildThumbHtml = (m) => { - let thumb = ''; - if (m.preview_url) { - const isVid = m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i); - if (isVid) thumb = ``; - else thumb = ``; - } else { - thumb = `
?
`; - } - return thumb; - }; - - fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=checkpoints,unet,diffusion_models`) - .then(r => r.json()).then(d => { - const buildMainDOM = (models) => { - mainGallery.innerHTML = ''; - if (!models || !models.length) { - mainGallery.innerHTML = 'No compatible main models found.'; - } else { - models.forEach(m => { - const isSelected = (data.mainModel && data.mainModel.filename === m.filename); - const card = document.createElement('div'); - card.className = 'anomalous-nb-minicheck ' + (isSelected ? 'selected' : ''); - card.innerHTML = `${buildThumbHtml(m)}
${m.filename}
`; - - if (m.preview_url && (m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i))) { - card.onmouseenter = () => { const v = card.querySelector('video'); if (v) v.play().catch(e => { }); }; - card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } }; - } - - card.onclick = () => { - data.mainModel = m; - this.saveCurrentNotebook(); - buildMainDOM(models); // re-render just the main gallery - }; - mainGallery.appendChild(card); - }); - } - }; - buildMainDOM(d.models || []); - }); - - fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=loras`) - .then(r => r.json()).then(d => { - const buildLoraDOM = (models) => { - loraGallery.innerHTML = ''; - if (!models || !models.length) { - loraGallery.innerHTML = 'No compatible Loras found.'; - } else { - models.forEach(m => { - const loraIndex = data.loras.findIndex(l => l.filename === m.filename); - const isSelected = loraIndex !== -1; - const card = document.createElement('div'); - card.className = 'anomalous-nb-minilora ' + (isSelected ? 'selected' : ''); - card.style.position = 'relative'; // for badge positioning - - let badgeHtml = ''; - if (isSelected) { - badgeHtml = `
${loraIndex + 1}
`; - } - - card.innerHTML = `${badgeHtml}${buildThumbHtml(m)}
${m.filename}
`; - - if (m.preview_url && (m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i))) { - card.onmouseenter = () => { const v = card.querySelector('video'); if (v) v.play().catch(e => { }); }; - card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } }; - } - - card.onclick = () => { - if (isSelected) { - data.loras = data.loras.filter(l => l.filename !== m.filename); - } else { - data.loras.push(m); - } - this.saveCurrentNotebook(); - buildLoraDOM(models); // re-render just the lora gallery - }; - loraGallery.appendChild(card); - }); - } - }; - buildLoraDOM(d.models || []); - }); - } - - sendNotebookToCanvas() { - if (!this.currentNotebook) return; - const data = this.currentNotebook.data || {}; - if (!data.mainModel) { - alert(currentLang === 'zh' ? "请先选择一个主模型。" : "Please select a Main Model first."); - return; - } - - const groupNodes = []; - const isUnet = data.mainModel.type === 'unet' || data.mainModel.type === 'diffusion_models'; - - const ckptNode = LiteGraph.createNode(isUnet ? "UNETLoader" : "CheckpointLoaderSimple"); - app.graph.add(ckptNode); - groupNodes.push({ node: ckptNode, relX: 0, relY: 0 }); - - const sub = data.mainModel.subfolder.replace(/^\/+/, '').replace(/\/+$/, ''); - const relPath = sub ? `${sub}/${data.mainModel.filename}` : data.mainModel.filename; - this.setWidgetValuePath(ckptNode, relPath); - - let lastNode = ckptNode; - let lastModelSlot = isUnet ? 0 : 0; - let lastClipSlot = isUnet ? null : 1; - - let relX = 350; - let relY = 0; - - data.loras.forEach((lora, idx) => { - const loraNode = LiteGraph.createNode("LoraLoader"); - app.graph.add(loraNode); - groupNodes.push({ node: loraNode, relX: relX, relY: relY }); - - const lsub = lora.subfolder.replace(/^\/+/, '').replace(/\/+$/, ''); - const lrelPath = lsub ? `${lsub}/${lora.filename}` : lora.filename; - this.setWidgetValuePath(loraNode, lrelPath); - - lastNode.connect(lastModelSlot, loraNode, 0); - if (lastClipSlot !== null) lastNode.connect(lastClipSlot, loraNode, 1); - - lastNode = loraNode; - lastModelSlot = 0; - lastClipSlot = 1; - relX += 350; - }); - - if (data.promptEn) { - const posNode = LiteGraph.createNode("CLIPTextEncode"); - posNode.title = "CLIP Text Encode (Positive)"; - app.graph.add(posNode); - groupNodes.push({ node: posNode, relX: relX, relY: 0 }); - - if (posNode.widgets && posNode.widgets.length > 0) { - const tw = posNode.widgets.find(w => w.name === 'text' || w.type === 'customtext'); - if (tw) tw.value = data.promptEn; - } - if (lastClipSlot !== null) { - lastNode.connect(lastClipSlot, posNode, 0); - } - - const negNode = LiteGraph.createNode("CLIPTextEncode"); - negNode.title = "CLIP Text Encode (Negative)"; - app.graph.add(negNode); - groupNodes.push({ node: negNode, relX: relX, relY: 250 }); - - if (negNode.widgets && negNode.widgets.length > 0) { - const tw = negNode.widgets.find(w => w.name === 'text' || w.type === 'customtext'); - if (tw) tw.value = "text, watermark, ugly, bad anatomy"; - } - if (lastClipSlot !== null) { - lastNode.connect(lastClipSlot, negNode, 0); - } - } - - this.nbPanel.style.display = 'none'; - this.close(); - - // Magnetic Sticking Logic - let isSticking = true; - const stickHandler = (e) => { - if (!isSticking || !app.canvas) return; - const canvas = app.canvas; - - let canvasX, canvasY; - if (canvas.convertEventToCanvasOffset) { - const pos = canvas.convertEventToCanvasOffset(e); - canvasX = pos[0]; - canvasY = pos[1]; - } else { - const rect = canvas.canvas.getBoundingClientRect(); - canvasX = (e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale; - canvasY = (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale; - } - - groupNodes.forEach(item => { - const w = (item.node.size && item.node.size[0]) ? item.node.size[0] : 200; - item.node.pos = [canvasX - (w / 2) + item.relX, canvasY - 20 + item.relY]; - }); - canvas.setDirty(true, true); - }; - - const dropHandler = (e) => { - if (!isSticking) return; - isSticking = false; - window.removeEventListener('mousemove', stickHandler, true); - window.removeEventListener('pointerdown', dropHandler, true); - window.removeEventListener('mousedown', dropHandler, true); - window.removeEventListener('click', dropHandler, true); - e.preventDefault(); - e.stopPropagation(); - }; - - window.addEventListener('mousemove', stickHandler, true); - setTimeout(() => { - window.addEventListener('pointerdown', dropHandler, true); - window.addEventListener('mousedown', dropHandler, true); - window.addEventListener('click', dropHandler, true); - }, 100); - } - setWidgetValuePath(node, relPath) { - if (!node.widgets || node.widgets.length === 0) return; - const w = node.widgets.find(wg => wg.type === 'combo'); - const targetWidget = w || node.widgets[0]; - if (!targetWidget) return; - - if (targetWidget.options && targetWidget.options.values) { - const normalizedTarget = relPath.replace(/\\/g, '/'); - const match = targetWidget.options.values.find(v => { - return String(v).replace(/\\/g, '/') === normalizedTarget; - }); - if (match) { - targetWidget.value = match; - return; - } - } - targetWidget.value = relPath; - } -} - -app.registerExtension({ - name: 'Anomalous.ModelBrowser', - async setup() { - const cssUrl = '/extensions/Anomalous_Model_Browser/styles.css?v=' + Date.now(); - if (!document.querySelector(`link[href^="/extensions/Anomalous_Model_Browser/styles.css"]`)) { - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.type = "text/css"; - link.href = cssUrl; - document.head.appendChild(link); - } - if (!localStorage.getItem('anomalous_lang')) { - try { - if (app && app.ui && app.ui.settings) { - const locale = app.ui.settings.getSettingValue('Comfy.Locale') || app.ui.settings.getSettingValue('Comfy.Locale.Language'); - if (locale) { - currentLang = locale.toLowerCase().includes('en') ? 'en' : 'zh'; - window.anomalous_browser_lang = currentLang; - } - } - } catch (e) { } - } - - const browser = new AnomalousBrowser(); - window.anomalousBrowserInstance = browser; - const btn = document.createElement('button'); - btn.id = 'anomalous-trigger-btn'; - btn.innerHTML = '📦'; - btn.title = 'Open Anomalous Model Browser'; - - let isDragging = false; - let startX, startY, initialX, initialY; - - btn.addEventListener('mousedown', (e) => { - isDragging = true; - startX = e.clientX; startY = e.clientY; - const rect = btn.getBoundingClientRect(); - initialX = rect.left; initialY = rect.top; - btn.style.transition = 'none'; - }); - window.addEventListener('mousemove', (e) => { - if (!isDragging) return; - e.preventDefault(); - let newX = initialX + (e.clientX - startX); - let newY = initialY + (e.clientY - startY); - if (newX < 0) newX = 0; - if (newY < 0) newY = 0; - if (newX > window.innerWidth - 60) newX = window.innerWidth - 60; - if (newY > window.innerHeight - 60) newY = window.innerHeight - 60; - btn.style.left = newX + 'px'; - btn.style.top = newY + 'px'; - btn.style.right = 'auto'; btn.style.bottom = 'auto'; - }); - window.addEventListener('mouseup', (e) => { - if (isDragging) { - isDragging = false; - btn.style.transition = 'transform 0.15s, box-shadow 0.15s'; - localStorage.setItem('anomalous_btn_x', btn.style.left); - localStorage.setItem('anomalous_btn_y', btn.style.top); - if (Math.abs(e.clientX - startX) < 5 && Math.abs(e.clientY - startY) < 5) browser.show(); - } - }); - - const savedX = localStorage.getItem('anomalous_btn_x'); - const savedY = localStorage.getItem('anomalous_btn_y'); - - const updateBtnBounds = () => { - let numX = parseInt(btn.style.left || savedX); - let numY = parseInt(btn.style.top || savedY); - if (isNaN(numX)) numX = window.innerWidth - 90; - if (isNaN(numY)) numY = window.innerHeight - 90; - - if (numX < 0) numX = 0; - if (numY < 0) numY = 0; - if (numX > window.innerWidth - 60) numX = window.innerWidth - 60; - if (numY > window.innerHeight - 60) numY = window.innerHeight - 60; - btn.style.left = numX + 'px'; - btn.style.top = numY + 'px'; - }; - - if (savedX && savedY && savedX !== 'NaN' && savedY !== 'NaN') { - btn.style.right = 'auto'; - btn.style.bottom = 'auto'; - btn.style.left = savedX; - btn.style.top = savedY; - } - - // Always trigger an update slightly after load to ensure it's in bounds - setTimeout(updateBtnBounds, 200); - - window.addEventListener('resize', () => { - if (btn.style.left) updateBtnBounds(); - }); - - document.body.appendChild(btn); - - // Pre-create a lightweight, translucent, and aesthetic drag ghost image for huge Hires Fix images - window.anomalousDragGhostImg = new Image(); - window.anomalousDragGhostImg.src = "data:image/svg+xml;utf8,W"; - } -});