try { if (typeof localStorage !== 'undefined') { localStorage.removeItem('anomalous_btn_x'); localStorage.removeItem('anomalous_btn_y'); } } catch (_) {} import { app } from "../../scripts/app.js"; import { createBrowserEntry } from "./modules/browser_entry.js?v=20260921-flicker-free-1"; import { createInterfaceSettings, getCurrentLanguage, setAbyssalScarletTheme, t } from "./modules/interface_settings.js"; export { setAbyssalScarletTheme }; const browserEntry = createBrowserEntry({ translate: t, getCurrentLanguage }); app.registerExtension({ name: "Anomalous.ModelBrowser", settings: [...browserEntry.settings, ...createInterfaceSettings()], actionBarButtons: browserEntry.actionBarButtons, commands: browserEntry.commands, keybindings: browserEntry.keybindings, menuCommands: browserEntry.menuCommands, setup: browserEntry.setup }); // --- INJECTED WORKFLOW SHARE MODULE --- // Workflow Share and Preview Modal for Anomalous_Model_Browser const AMB_WorkflowShare = { // ---------------------------------------------------------------------- // 1. Data Compression & Base64 Utils // ---------------------------------------------------------------------- strToU8(str) { return new TextEncoder().encode(str); }, u8ToStr(u8) { return new TextDecoder().decode(u8); }, u8ToBase64(u8) { let binary = ''; const len = u8.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(u8[i]); } return window.btoa(binary); }, base64ToU8(b64) { const binary = window.atob(b64); const len = binary.length; const u8 = new Uint8Array(len); for (let i = 0; i < len; i++) { u8[i] = binary.charCodeAt(i); } return u8; }, async compress(str) { const stream = new Blob([this.strToU8(str)]).stream(); const compressedStream = stream.pipeThrough(new CompressionStream('deflate-raw')); const response = new Response(compressedStream); const blob = await response.blob(); const buffer = await blob.arrayBuffer(); return new Uint8Array(buffer); }, async decompress(u8) { const stream = new Blob([u8]).stream(); const decompressedStream = stream.pipeThrough(new DecompressionStream('deflate-raw')); const response = new Response(decompressedStream); const blob = await response.blob(); const buffer = await blob.arrayBuffer(); return this.u8ToStr(new Uint8Array(buffer)); }, // ---------------------------------------------------------------------- // 2. Skeleton Generation (Strip visual / default data) // ---------------------------------------------------------------------- skeletonize(workflowJson) { const wf = JSON.parse(JSON.stringify(workflowJson)); // deep copy // ComfyUI workflow JSON format has "nodes" array if (wf.nodes && Array.isArray(wf.nodes)) { wf.nodes.forEach(node => { // Delete layout coords and styles delete node.pos; delete node.size; delete node.color; delete node.bgcolor; delete node.shape; delete node.flags; // Delete empty properties if (node.properties && Object.keys(node.properties).length === 0) { delete node.properties; } }); } // Remove view metadata if (wf.extra) { delete wf.extra.ds; // scale/offset } return wf; }, // ---------------------------------------------------------------------- // 3. Auto-Layout Algorithm // ---------------------------------------------------------------------- autoLayout(workflowJson) { if (!workflowJson.nodes || !Array.isArray(workflowJson.nodes)) return workflowJson; const nodes = workflowJson.nodes; // 1. Build adjacency list and in-degrees const adj = new Map(); const inDegree = new Map(); nodes.forEach(n => { adj.set(n.id, []); if (!inDegree.has(n.id)) inDegree.set(n.id, 0); }); // Check links if (workflowJson.links) { workflowJson.links.forEach(link => { if (!link) return; const fromId = link[1]; const toId = link[3]; if (adj.has(fromId) && adj.has(toId)) { adj.get(fromId).push(toId); inDegree.set(toId, inDegree.get(toId) + 1); } }); } // 2. Topological sort with depth levels const depthMap = new Map(); // id -> depth const queue = []; nodes.forEach(n => { if (inDegree.get(n.id) === 0) { queue.push(n.id); depthMap.set(n.id, 0); } }); while (queue.length > 0) { const curr = queue.shift(); const currDepth = depthMap.get(curr); const neighbors = adj.get(curr); if (neighbors) { neighbors.forEach(nxt => { // Reduce in-degree const ind = inDegree.get(nxt) - 1; inDegree.set(nxt, ind); // Update depth to be max(existing depth, currDepth + 1) const existingDepth = depthMap.get(nxt) || 0; depthMap.set(nxt, Math.max(existingDepth, currDepth + 1)); if (ind === 0) { queue.push(nxt); } }); } } // Handle cycles (nodes not reached) nodes.forEach(n => { if (!depthMap.has(n.id)) { depthMap.set(n.id, 0); } }); // 3. Assign X, Y coordinates const nodesByDepth = {}; nodes.forEach(n => { const d = depthMap.get(n.id); if (!nodesByDepth[d]) nodesByDepth[d] = []; nodesByDepth[d].push(n); }); // Spacing constants const X_SPACING = 400; const Y_SPACING = 300; Object.keys(nodesByDepth).forEach(d => { const levelNodes = nodesByDepth[d]; const depth = parseInt(d); levelNodes.forEach((n, idx) => { // Approximate size n.pos = [ depth * X_SPACING, idx * Y_SPACING ]; }); }); return workflowJson; }, // ---------------------------------------------------------------------- // 4. Encode / Decode // ---------------------------------------------------------------------- async encodeShareCode(workflowJson, isSkeleton) { let targetJson = workflowJson; if (isSkeleton) { targetJson = this.skeletonize(workflowJson); } const jsonStr = JSON.stringify(targetJson); const compressedU8 = await this.compress(jsonStr); const base64Str = this.u8ToBase64(compressedU8); const prefix = isSkeleton ? 'AMB1-' : 'AMB0-'; return prefix + base64Str; }, async decodeShareCode(shareCode) { if (!shareCode.startsWith('AMB0-') && !shareCode.startsWith('AMB1-')) { throw new Error('Invalid Share Code Format.'); } const isSkeleton = shareCode.startsWith('AMB1-'); const base64Str = shareCode.substring(5); const compressedU8 = this.base64ToU8(base64Str); const jsonStr = await this.decompress(compressedU8); let workflowJson = JSON.parse(jsonStr); if (isSkeleton) { workflowJson = this.autoLayout(workflowJson); } return workflowJson; }, // ---------------------------------------------------------------------- // 5. UI Modals // ---------------------------------------------------------------------- showToast(message, color) { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 30px; right: 30px; background: #2a2a2b; color: ${color || '#fff'}; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); font-family: Arial, sans-serif; font-size: 14px; z-index: 9999999; opacity: 0; transition: opacity 0.3s ease; border-left: 4px solid ${color || '#fff'}; `; document.body.appendChild(toast); setTimeout(() => toast.style.opacity = '1', 10); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3000); }, showExportModal() { const overlay = document.createElement('div'); overlay.id = 'amb-export-modal'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 999999; display: flex; justify-content: center; align-items: center; font-family: Arial, sans-serif; `; const content = document.createElement('div'); content.style.cssText = ` background: #2a2a2b; color: #fff; padding: 30px; border-radius: 12px; width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; gap: 20px; `; const title = document.createElement('h2'); title.style.margin = '0'; title.textContent = t('mainExportTitle'); const typeSelectContainer = document.createElement('div'); typeSelectContainer.innerHTML = ` `; const textArea = document.createElement('textarea'); textArea.style.cssText = ` width: 100%; height: 150px; background: #1e1e1f; color: #eee; border: 1px solid #444; border-radius: 6px; padding: 10px; font-family: monospace; font-size: 12px; resize: none; box-sizing: border-box; `; textArea.readOnly = true; const btnGroup = document.createElement('div'); btnGroup.style.cssText = `display: flex; gap: 10px; justify-content: flex-end;`; const generateBtn = document.createElement('button'); generateBtn.textContent = t('mainGenerate'); generateBtn.style.cssText = `padding: 8px 16px; background: #4a90e2; color: #fff; border: none; border-radius: 6px; cursor: pointer;`; const copyBtn = document.createElement('button'); copyBtn.textContent = t('mainCopyClipboard'); copyBtn.style.cssText = `padding: 8px 16px; background: #5cb85c; color: #fff; border: none; border-radius: 6px; cursor: pointer; display: none;`; const closeBtn = document.createElement('button'); closeBtn.textContent = t('mainClose'); closeBtn.style.cssText = `padding: 8px 16px; background: #555; color: #fff; border: none; border-radius: 6px; cursor: pointer;`; closeBtn.onclick = () => overlay.remove(); generateBtn.onclick = async () => { const isSkeleton = document.querySelector('input[name="amb-share-type"]:checked').value === 'skeleton'; // Get current workflow from app graph const p = await app.graphToPrompt(); const workflowJson = p.workflow; try { const code = await AMB_WorkflowShare.encodeShareCode(workflowJson, isSkeleton); textArea.value = code; copyBtn.style.display = 'block'; } catch (err) { textArea.value = 'Error generating code: ' + err.message; } }; copyBtn.onclick = () => { textArea.select(); document.execCommand('copy'); AMB_WorkflowShare.showToast(t('mainCopied'), '#5cb85c'); }; btnGroup.appendChild(generateBtn); btnGroup.appendChild(copyBtn); btnGroup.appendChild(closeBtn); content.appendChild(title); content.appendChild(typeSelectContainer); content.appendChild(textArea); content.appendChild(btnGroup); overlay.appendChild(content); document.body.appendChild(overlay); }, showImportModal() { const overlay = document.createElement('div'); overlay.id = 'amb-import-modal'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 999999; display: flex; justify-content: center; align-items: center; font-family: Arial, sans-serif; `; const content = document.createElement('div'); content.style.cssText = ` background: #2a2a2b; color: #fff; padding: 30px; border-radius: 12px; width: 600px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; gap: 20px; `; const title = document.createElement('h2'); title.style.margin = '0'; title.textContent = t('mainImportTitle'); const inputArea = document.createElement('textarea'); inputArea.placeholder = t('mainSharePlaceholder'); inputArea.style.cssText = ` width: 100%; height: 100px; background: #1e1e1f; color: #eee; border: 1px solid #444; border-radius: 6px; padding: 10px; font-family: monospace; font-size: 12px; resize: none; box-sizing: border-box; `; const btnGroup = document.createElement('div'); btnGroup.style.cssText = `display: flex; gap: 10px; justify-content: flex-end;`; const loadBtn = document.createElement('button'); loadBtn.textContent = t('mainImportLoad'); loadBtn.style.cssText = `padding: 8px 16px; background: #e07a5f; color: #fff; border: none; border-radius: 6px; cursor: pointer;`; const closeBtn = document.createElement('button'); closeBtn.textContent = t('mainCancel'); closeBtn.style.cssText = `padding: 8px 16px; background: #555; color: #fff; border: none; border-radius: 6px; cursor: pointer;`; closeBtn.onclick = () => overlay.remove(); loadBtn.onclick = async () => { const code = inputArea.value.trim(); if (!code) { AMB_WorkflowShare.showToast(t('mainShareEmpty'), '#ff6b6b'); return; } try { const pendingWorkflow = await AMB_WorkflowShare.decodeShareCode(code); app.loadGraphData(pendingWorkflow); overlay.remove(); const nodesCount = pendingWorkflow.nodes ? pendingWorkflow.nodes.length : 0; AMB_WorkflowShare.showToast(t('mainImportedNodes', { count: nodesCount }), '#5cb85c'); // Auto close the main browser panel const mainCloseBtn = document.getElementById('anomalous-close'); if (mainCloseBtn) mainCloseBtn.click(); } catch (err) { AMB_WorkflowShare.showToast(t('mainDecodeFailed') + err.message, '#ff6b6b'); } }; btnGroup.appendChild(loadBtn); btnGroup.appendChild(closeBtn); content.appendChild(title); content.appendChild(inputArea); content.appendChild(btnGroup); overlay.appendChild(content); document.body.appendChild(overlay); }, showUnifiedModal() { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 999999; display: flex; justify-content: center; align-items: center; font-family: Arial, sans-serif; `; const content = document.createElement('div'); content.style.cssText = ` background: #2a2a2b; color: #fff; padding: 30px; border-radius: 12px; width: 400px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; gap: 20px; text-align: center; `; const title = document.createElement('h2'); title.style.margin = '0'; title.textContent = t('mainUnifiedTitle'); const exportBtn = document.createElement('button'); exportBtn.textContent = t('mainExportWorkflow'); exportBtn.style.cssText = `padding: 12px; background: #4a90e2; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;`; exportBtn.onclick = () => { overlay.remove(); this.showExportModal(); }; const importBtn = document.createElement('button'); importBtn.textContent = t('mainImportWorkflow'); importBtn.style.cssText = `padding: 12px; background: #e07a5f; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;`; importBtn.onclick = () => { overlay.remove(); this.showImportModal(); }; const closeBtn = document.createElement('button'); closeBtn.textContent = t('mainClose'); closeBtn.style.cssText = `padding: 8px; background: #555; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; margin-top: 10px;`; closeBtn.onclick = () => overlay.remove(); content.appendChild(title); content.appendChild(exportBtn); content.appendChild(importBtn); content.appendChild(closeBtn); overlay.appendChild(content); document.body.appendChild(overlay); } }; window.AMB_WorkflowShare = AMB_WorkflowShare;