Files
DemonGatanjieu-Anomalous_Mo…/web/main.js
T
DemonGatanjieu 77ae7ce888 fix(ui): keep update guide manual and clean sidebar styles
Keep a persistent header guide button, restore upward label transitions, remove duplicate CSS and obsolete notice events, and move replay styling to CSS.

Document proportional UX checks and interruption recovery checkpoints. Validated update_guide.mjs, 48-module parse/link check, diff check, and live ComfyUI manual open, Escape, replay, Help and sidebar keyboard labels. No backend behavior changed; mobile and alternate themes were not exercised.
2026-09-16 11:27:19 +08:00

1414 lines
62 KiB
JavaScript

import { showDetail, showEditModal, _openAdvancedModelSelector, setWidgetValuePath } from './modules/ui_detail.js';
import { loadModels, applyModelToCanvas, stopMediaInContainer } from './modules/ui_grid.js';
import { createDOM, renderSidebar, loadFolders, showHelp, hideAllPanels, openFolderManager } from './modules/ui_sidebar.js';
import { loadGalleryImages, refreshGalleryImages, showGeneratedGallery, showGallerySelectMode, showGalleryViewer } from './modules/ui_gallery.js';
import { showNotebooks, closeWorkspace, refreshNotebooks, saveCurrentNotebook, deleteCurrentNotebook, renderNotebookEditor, fillNotebookGalleries, sendNotebookToCanvas } from './modules/ui_notebooks.js';
import { showRecipes, refreshRecipes, renderRecipeList, handleSaveRecipe } from './modules/ui_recipes.js';
import { showMaterials, refreshMaterials, openSavedMaterial, openMaterialLibrary } from './modules/ui_materials.js';
import { openPromptStudio } from './modules/ui_prompt_composer.js';
import { openPromptTranslator } from './modules/ui_prompt_translator.js';
import { closeUpdateGuide } from './modules/ui_update_guide.js';
import { showImageWorkbench } from './modules/ui_gallery_detail.js';
import { initDoctorPanel, diagnoseNode, renderGlobalDashboard, initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, _openGalleryReplacer, openLoraInsertionPicker, runGlobalDoctorScan } from './modules/ui_doctor.js';
import { app } from "../../scripts/app.js";
import { normalizeLocale, resolveLocale, translate } from './modules/locales.js';
import {
clampFloatingTriggerPosition,
normalizeEntryMode,
normalizeFloatingTriggerSize,
normalizeFloatingTriggerStyle
} from './modules/entry_controls.js';
import {
createShortcutSettingControl,
DEFAULT_BROWSER_SHORTCUT
} from './modules/shortcut_controls.js';
// ============================================================================
// TABLE OF CONTENTS (TOC)
// 1. App Registration & Entry (Search for "app.registerExtension")
// 2. State & Class Constructor (Search for "class AnomalousBrowser")
// 3. UI - Sidebar (Search for "createDOM")
// 4. UI - Main Grid (Search for "renderGrid")
// 5. UI - Detail Panel (Search for "showDetail")
// 6. UI - Gallery Viewer (Search for "createGalleryViewer")
// 7. UI - Doctor Panel (Search for "createDoctorPanel")
// 8. Notebooks & Workflow Recipes (Search for "Notebook" or "Recipes")
// ============================================================================
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 = resolveLocale(localStorage.getItem('anomalous_lang') || defaultLang);
window.anomalous_browser_lang = currentLang;
const t = (key, params) => translate(key, params, window.anomalous_browser_lang || currentLang);
const LANGUAGE_SETTING_ID = 'Anomalous.ModelBrowser.Language';
const ENTRY_MODE_SETTING_ID = 'Anomalous.ModelBrowser.EntryMode';
const SHORTCUT_SETTING_ID = 'Anomalous.ModelBrowser.Shortcut';
const FLOATING_TRIGGER_SIZE_SETTING_ID = 'Anomalous.ModelBrowser.FloatingTriggerSize';
const FLOATING_TRIGGER_STYLE_SETTING_ID = 'Anomalous.ModelBrowser.FloatingTriggerStyle';
const ABYSSAL_SCARLET_SETTING_ID = 'Anomalous.ModelBrowser.AbyssalScarletTheme';
const OPEN_BROWSER_COMMAND_ID = 'Anomalous.ModelBrowser.Open';
const RESET_TRIGGER_POSITION_COMMAND_ID = 'Anomalous.ModelBrowser.ResetFloatingTriggerPosition';
let entryMode = 'floating';
let floatingTriggerSize = 'medium';
let floatingTriggerStyle = 'icon';
let browserInstance = null;
let triggerButton = null;
let triggerBoundsUpdater = null;
function normalizeLanguagePreference(value) {
return value === 'zh' || value === 'en' ? value : 'auto';
}
function resolveComfyLanguage() {
try {
const settings = app.extensionManager?.setting;
const locale = settings?.get('Comfy.Locale')
|| app.ui?.settings?.getSettingValue?.('Comfy.Locale')
|| app.ui?.settings?.getSettingValue?.('Comfy.Locale.Language');
return normalizeLocale(locale) || defaultLang;
} catch (error) {
return defaultLang;
}
}
function getInterfaceSettingTranslations() {
const category = t('mainInterfaceCategory');
return {
[FLOATING_TRIGGER_STYLE_SETTING_ID]: {
name: t('mainFloatingTriggerStyleSetting'),
category: ['Anomalous Model Browser', category, 'floating-trigger-style'],
tooltip: t('mainFloatingTriggerStyleTooltip'),
options: [
{ value: 'icon', text: t('mainFloatingTriggerStyleIcon') },
{ value: 'pill', text: t('mainFloatingTriggerStylePill') }
]
},
[FLOATING_TRIGGER_SIZE_SETTING_ID]: {
name: t('mainFloatingTriggerSizeSetting'),
category: ['Anomalous Model Browser', category, 'floating-trigger-size'],
tooltip: t('mainFloatingTriggerSizeTooltip'),
options: [
{ value: 'small', text: t('mainFloatingTriggerSizeSmall') },
{ value: 'medium', text: t('mainFloatingTriggerSizeMedium') },
{ value: 'large', text: t('mainFloatingTriggerSizeLarge') }
]
},
[SHORTCUT_SETTING_ID]: {
name: t('mainShortcutSetting'),
category: ['Anomalous Model Browser', category, 'shortcut'],
tooltip: t('mainShortcutTooltip')
},
[ENTRY_MODE_SETTING_ID]: {
name: t('mainEntryModeSetting'),
category: ['Anomalous Model Browser', category, 'entry-mode'],
tooltip: t('mainEntryModeTooltip'),
options: [
{ value: 'floating', text: t('mainEntryModeFloating') },
{ value: 'topbar', text: t('mainEntryModeTopbar') },
{ value: 'menu', text: t('mainEntryModeMenu') }
]
},
[LANGUAGE_SETTING_ID]: {
name: t('mainLanguageSetting'),
category: ['Anomalous Model Browser', category, 'language'],
tooltip: t('mainLanguageTooltip'),
options: [
{ value: 'auto', text: t('mainLanguageAuto') },
{ value: 'zh', text: t('mainLanguageChinese') },
{ value: 'en', text: t('mainLanguageEnglish') }
]
},
[ABYSSAL_SCARLET_SETTING_ID]: {
name: t('mainAbyssalScarletThemeSetting'),
category: ['Anomalous Model Browser', category, 'theme'],
tooltip: t('mainAbyssalScarletThemeTooltip')
}
};
}
function refreshRegisteredInterfaceSettings() {
const settingsApi = app.extensionManager?.setting;
const registry = settingsApi?.settings?.value || settingsApi?.settings;
if (!registry || typeof registry !== 'object') return;
const translations = getInterfaceSettingTranslations();
for (const [id, patch] of Object.entries(translations)) {
const current = registry[id];
if (!current) continue;
// Replacing the descriptor keeps ComfyUI's computed settings tree reactive,
// including the open settings dialog and its translated combo options.
registry[id] = { ...current, ...patch };
}
}
function applyLanguagePreference(value) {
const preference = normalizeLanguagePreference(value);
if (preference === 'auto') {
localStorage.removeItem('anomalous_lang');
} else {
localStorage.setItem('anomalous_lang', preference);
}
const nextLanguage = preference === 'auto' ? resolveComfyLanguage() : preference;
const changed = nextLanguage !== window.anomalous_browser_lang;
currentLang = nextLanguage;
window.anomalous_browser_lang = nextLanguage;
applyFloatingTriggerPresentation();
refreshRegisteredInterfaceSettings();
if (changed) {
window.dispatchEvent(new CustomEvent('anomalous-language-change', {
detail: { language: nextLanguage, preference }
}));
}
}
if (!localStorage.getItem('anomalous_lang')) {
currentLang = resolveComfyLanguage();
window.anomalous_browser_lang = currentLang;
}
function showThemeNoticeToast(isEnabled) {
const existing = document.getElementById('anomalous-theme-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'anomalous-theme-toast';
toast.className = 'anomalous-theme-toast' + (isEnabled ? ' is-abyssal' : '');
toast.textContent = isEnabled ? t('themeDomainActivated') : t('themeDomainDeactivated');
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add('is-show');
});
setTimeout(() => {
toast.classList.remove('is-show');
setTimeout(() => toast.remove(), 400);
}, 2400);
}
export function setAbyssalScarletTheme(enabled, notify = false) {
const isEnabled = Boolean(enabled);
localStorage.setItem('anomalous_theme_abyssal_scarlet', isEnabled ? 'true' : 'false');
document.documentElement.classList.toggle('theme-abyssal-scarlet', isEnabled);
const modal = document.getElementById('anomalous-modal');
if (modal) modal.classList.toggle('theme-abyssal-scarlet', isEnabled);
const container = document.getElementById('anomalous-container');
if (container) container.classList.toggle('theme-abyssal-scarlet', isEnabled);
try {
const settings = app.extensionManager?.setting;
if (settings && typeof settings.set === 'function') {
if (settings.get(ABYSSAL_SCARLET_SETTING_ID) !== isEnabled) {
settings.set(ABYSSAL_SCARLET_SETTING_ID, isEnabled);
}
}
} catch (_) {}
if (notify) {
showThemeNoticeToast(isEnabled);
}
window.dispatchEvent(new CustomEvent('anomalous-theme-change', {
detail: { theme: isEnabled ? 'abyssal-scarlet' : 'default', enabled: isEnabled }
}));
}
window.setAbyssalScarletTheme = setAbyssalScarletTheme;
const initialTheme = localStorage.getItem('anomalous_theme_abyssal_scarlet') === 'true';
setAbyssalScarletTheme(initialTheme, false);
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.cardThumbnailMode = localStorage.getItem('anomalous_card_thumbnail_mode') === 'original'
? 'original'
: 'balanced';
this.createDOM();
}
show() {
if (this._idleReleaseTimer) {
clearTimeout(this._idleReleaseTimer);
this._idleReleaseTimer = null;
}
this.setTriggerVisible(false);
this.modal.classList.add('visible');
if (!this.foldersData) {
this.loadFolders();
} else {
this.loadModels();
}
}
close() {
closeUpdateGuide(this);
this.modal.classList.remove('visible');
this.setTriggerVisible(true);
const canvas = document.getElementById('graph-canvas');
if (canvas instanceof HTMLElement) canvas.focus({ preventScroll: true });
if (this._modelLoadController) this._modelLoadController.abort();
if (this._modelMediaObserver) this._modelMediaObserver.disconnect();
this.modal.querySelectorAll('video, audio').forEach(media => media.pause());
this.stopMediaInContainer(this.grid);
if (this._idleReleaseTimer) clearTimeout(this._idleReleaseTimer);
this._idleReleaseTimer = setTimeout(() => {
if (this.modal.classList.contains('visible')) return;
this.stopMediaInContainer(this.grid);
this.grid.replaceChildren();
this.models = [];
}, 90000);
}
setTriggerVisible(visible) {
const trigger = this.triggerButton || document.getElementById('anomalous-trigger-btn');
trigger?.classList.toggle('anomalous-trigger-hidden', !visible || entryMode !== 'floating');
}
// [Doctor & Assistant Panel methods extracted to ui_doctor.js]
showPreflightImportModal() {
let overlay = document.getElementById('anomalous-import-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'anomalous-import-overlay';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100vw';
overlay.style.height = '100vh';
overlay.style.backgroundColor = 'rgba(0,0,0,0.85)';
overlay.style.zIndex = '9999999';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
const modal = document.createElement('div');
modal.style.width = '600px';
modal.style.maxWidth = '90vw';
modal.style.background = '#222';
modal.style.borderRadius = '12px';
modal.style.padding = '20px';
modal.style.border = '1px solid rgba(255,255,255,0.1)';
modal.style.boxShadow = '0 10px 30px rgba(0,0,0,0.5)';
modal.style.display = 'flex';
modal.style.flexDirection = 'column';
modal.style.gap = '15px';
const header = document.createElement('h2');
header.style.margin = '0';
header.style.color = '#fff';
header.textContent = t('mainPreflightTitle');
modal.appendChild(header);
const desc = document.createElement('div');
desc.style.color = '#aaa';
desc.style.fontSize = '14px';
desc.textContent = t('mainPreflightDesc');
modal.appendChild(desc);
const textarea = document.createElement('textarea');
textarea.id = 'anomalous-import-textarea';
textarea.style.width = '100%';
textarea.style.height = '150px';
textarea.style.background = '#111';
textarea.style.color = '#fff';
textarea.style.border = '1px solid #444';
textarea.style.borderRadius = '6px';
textarea.style.padding = '10px';
textarea.style.fontFamily = 'monospace';
textarea.style.resize = 'vertical';
textarea.style.boxSizing = 'border-box';
textarea.placeholder = '{"last_node_id": ...}';
modal.appendChild(textarea);
const resultArea = document.createElement('div');
resultArea.id = 'anomalous-import-result';
resultArea.style.maxHeight = '300px';
resultArea.style.overflowY = 'auto';
resultArea.style.display = 'none';
resultArea.style.flexDirection = 'column';
resultArea.style.gap = '10px';
resultArea.style.background = 'rgba(0,0,0,0.2)';
resultArea.style.padding = '15px';
resultArea.style.borderRadius = '8px';
modal.appendChild(resultArea);
const btnRow = document.createElement('div');
btnRow.style.display = 'flex';
btnRow.style.justifyContent = 'flex-end';
btnRow.style.gap = '10px';
const closeBtn = document.createElement('button');
closeBtn.textContent = t('mainCancel');
closeBtn.style.padding = '8px 16px';
closeBtn.style.background = 'transparent';
closeBtn.style.color = '#ccc';
closeBtn.style.border = '1px solid #555';
closeBtn.style.borderRadius = '6px';
closeBtn.style.cursor = 'pointer';
closeBtn.onclick = () => { overlay.style.display = 'none'; };
const analyzeBtn = document.createElement('button');
analyzeBtn.id = 'anomalous-import-analyze-btn';
analyzeBtn.textContent = t('mainAnalyzePreflight');
analyzeBtn.style.padding = '8px 16px';
analyzeBtn.style.background = '#1a73e8';
analyzeBtn.style.color = '#fff';
analyzeBtn.style.border = 'none';
analyzeBtn.style.borderRadius = '6px';
analyzeBtn.style.cursor = 'pointer';
const loadBtn = document.createElement('button');
loadBtn.id = 'anomalous-import-load-btn';
loadBtn.textContent = t('mainForceLoad');
loadBtn.style.padding = '8px 16px';
loadBtn.style.background = '#28a745';
loadBtn.style.color = '#fff';
loadBtn.style.border = 'none';
loadBtn.style.borderRadius = '6px';
loadBtn.style.cursor = 'pointer';
loadBtn.style.display = 'none';
let parsedGraphData = null;
analyzeBtn.onclick = async () => {
try {
const jsonStr = textarea.value.trim();
if (!jsonStr) return;
parsedGraphData = JSON.parse(jsonStr);
try {
const res = await fetch('/anomalous/all_hashes');
const hData = await res.json();
const hObj = hData.hashes ? hData.hashes : hData;
Object.assign(window.anomalous_hash_cache, hObj);
} catch (e) { }
const models = [];
if (parsedGraphData.nodes) {
for (const node of parsedGraphData.nodes) {
if (node.widgets_values) {
for (const v of node.widgets_values) {
if (typeof v === 'string' && (v.endsWith('.safetensors') || v.endsWith('.ckpt') || v.endsWith('.pt') || v.endsWith('.sft') || v.endsWith('.bin'))) {
models.push({ nodeType: node.type, value: v });
}
}
}
}
}
textarea.style.display = 'none';
resultArea.style.display = 'flex';
analyzeBtn.style.display = 'none';
loadBtn.style.display = 'block';
if (models.length === 0) {
const noDependencies = document.createElement('div');
noDependencies.style.color = '#28a745';
noDependencies.textContent = t('mainNoDependencies');
resultArea.replaceChildren(noDependencies);
return;
}
const detectedHeader = document.createElement('div');
detectedHeader.style.color = '#fff';
detectedHeader.style.fontWeight = 'bold';
detectedHeader.style.marginBottom = '10px';
detectedHeader.textContent = t('mainDetectedModels', { count: models.length });
resultArea.replaceChildren(detectedHeader);
for (const m of models) {
const val = m.value;
const parts = val.split(/[\\/]/);
const basename = parts[parts.length - 1];
let isHealthy = false;
let cacheHit = window.anomalous_hash_cache[val] || window.anomalous_hash_cache[basename];
const fetchRes = await fetch(`/anomalous/resolve_hash?hash=unknown&size=&filename=${encodeURIComponent(val)}`);
const fetchData = await fetchRes.json();
if (fetchData.found) {
isHealthy = true;
} else if (cacheHit) {
const resolveRes = await fetch(`/anomalous/resolve_hash?hash=${encodeURIComponent(cacheHit.hash || cacheHit)}&size=`);
const resolveData = await resolveRes.json();
if (resolveData.found) isHealthy = true;
}
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.justifyContent = 'space-between';
item.style.padding = '8px';
item.style.background = 'rgba(255,255,255,0.05)';
item.style.borderRadius = '4px';
const left = document.createElement('div');
left.style.display = 'flex';
left.style.alignItems = 'center';
left.style.gap = '8px';
const icon = document.createElement('span');
icon.innerText = isHealthy ? '✅' : '⚠️';
const text = document.createElement('span');
text.innerText = `[${m.nodeType}] ${basename}`;
text.style.color = isHealthy ? '#ccc' : '#ff6b6b';
text.style.fontSize = '14px';
left.appendChild(icon);
left.appendChild(text);
item.appendChild(left);
if (!isHealthy) {
const right = document.createElement('a');
const normVal = val.replace(/\\/g, '/');
const winVal = val.replace(/\//g, '\\');
let workflowHash = null;
if (app.graph && app.graph.extra && app.graph.extra.anomalous_hashes) {
const hData = app.graph.extra.anomalous_hashes[`${m.nodeId || ''}_${val}`] ||
app.graph.extra.anomalous_hashes[`${m.nodeId || ''}_${normVal}`] ||
app.graph.extra.anomalous_hashes[`${m.nodeId || ''}_${winVal}`] ||
app.graph.extra.anomalous_hashes[val] ||
app.graph.extra.anomalous_hashes[normVal] ||
app.graph.extra.anomalous_hashes[winVal];
if (hData) workflowHash = typeof hData === 'string' ? hData : hData.hash;
}
const searchHash = workflowHash || (cacheHit ? (typeof cacheHit === 'string' ? cacheHit : cacheHit.hash) : null);
const searchStr = basename.replace('.safetensors', '').replace('.ckpt', '').replace('.pt', '').replace('.sft', '');
right.href = `https://civitai.com/search/models?sortBy=models_v9&query=${encodeURIComponent(searchHash || searchStr)}`;
right.target = '_blank';
right.textContent = t('mainDownloadCivitai');
right.style.color = '#1a73e8';
right.style.fontSize = '12px';
right.style.textDecoration = 'none';
right.style.cursor = 'pointer';
if (searchHash) {
right.onclick = async (e) => {
e.preventDefault();
const prevText = right.textContent;
right.textContent = '⏳...';
try {
const res = await fetch(`https://civitai.com/api/v1/model-versions/by-hash/${encodeURIComponent(searchHash)}`);
if (res.ok) {
const data = await res.json();
if (data && data.modelId) {
const nsfwLevel = data.nsfwLevel || 1;
const isNsfw = (data.model && data.model.nsfw) || (nsfwLevel > 1);
const domain = isNsfw ? 'civitai.red' : 'civitai.com';
let targetUrl = `https://${domain}/models/${data.modelId}`;
if (data.id) targetUrl += `?modelVersionId=${data.id}`;
window.open(targetUrl, '_blank');
right.textContent = prevText;
return;
}
}
} catch (err) {
console.warn("[Anomalous] Civitai by-hash lookup failed:", err);
}
right.textContent = prevText;
window.open(`https://civitai.com/search/models?sortBy=models_v9&query=${encodeURIComponent(searchStr)}`, '_blank');
};
}
item.appendChild(right);
} else {
const right = document.createElement('span');
right.textContent = t('mainReady');
right.style.color = '#28a745';
right.style.fontSize = '12px';
item.appendChild(right);
}
resultArea.appendChild(item);
}
} catch (e) {
alert(t('mainInvalidJson') + ' ' + e.message);
}
};
loadBtn.onclick = () => {
if (parsedGraphData && app) {
app.loadGraphData(parsedGraphData);
overlay.style.display = 'none';
}
};
btnRow.appendChild(closeBtn);
btnRow.appendChild(analyzeBtn);
btnRow.appendChild(loadBtn);
modal.appendChild(btnRow);
overlay.appendChild(modal);
document.body.appendChild(overlay);
}
if (textarea) {
textarea.value = '';
textarea.style.display = 'block';
}
if (resultArea) {
resultArea.style.display = 'none';
resultArea.innerHTML = '';
}
if (analyzeBtn) analyzeBtn.style.display = 'block';
if (loadBtn) loadBtn.style.display = 'none';
overlay.style.display = 'flex';
}
}
// --- Extracted Module Bindings ---
AnomalousBrowser.prototype.initDoctorPanel = initDoctorPanel;
AnomalousBrowser.prototype.diagnoseNode = diagnoseNode;
AnomalousBrowser.prototype.renderGlobalDashboard = renderGlobalDashboard;
AnomalousBrowser.prototype.initAssistantPanel = initAssistantPanel;
AnomalousBrowser.prototype.renderAssistantModelCard = renderAssistantModelCard;
AnomalousBrowser.prototype._loadAssistantHistory = _loadAssistantHistory;
AnomalousBrowser.prototype._openGalleryReplacer = _openGalleryReplacer;
AnomalousBrowser.prototype.openLoraInsertionPicker = openLoraInsertionPicker;
AnomalousBrowser.prototype.runGlobalDoctorScan = runGlobalDoctorScan;
AnomalousBrowser.prototype.showNotebooks = showNotebooks;
AnomalousBrowser.prototype.closeWorkspace = closeWorkspace;
AnomalousBrowser.prototype.refreshNotebooks = refreshNotebooks;
AnomalousBrowser.prototype.saveCurrentNotebook = saveCurrentNotebook;
AnomalousBrowser.prototype.deleteCurrentNotebook = deleteCurrentNotebook;
AnomalousBrowser.prototype.renderNotebookEditor = renderNotebookEditor;
AnomalousBrowser.prototype.fillNotebookGalleries = fillNotebookGalleries;
AnomalousBrowser.prototype.sendNotebookToCanvas = sendNotebookToCanvas;
AnomalousBrowser.prototype.showRecipes = showRecipes;
AnomalousBrowser.prototype.refreshRecipes = refreshRecipes;
AnomalousBrowser.prototype.renderRecipeList = renderRecipeList;
AnomalousBrowser.prototype.handleSaveRecipe = handleSaveRecipe;
AnomalousBrowser.prototype.showMaterials = showMaterials;
AnomalousBrowser.prototype.openSavedMaterial = openSavedMaterial;
AnomalousBrowser.prototype.openMaterialLibrary = openMaterialLibrary;
AnomalousBrowser.prototype.openPromptStudio = openPromptStudio;
AnomalousBrowser.prototype.openPromptTranslator = function() { openPromptTranslator(this); };
AnomalousBrowser.prototype.refreshMaterials = refreshMaterials;
AnomalousBrowser.prototype.loadGalleryImages = loadGalleryImages;
AnomalousBrowser.prototype.refreshGalleryImages = refreshGalleryImages;
AnomalousBrowser.prototype.showGeneratedGallery = showGeneratedGallery;
AnomalousBrowser.prototype.showGallerySelectMode = showGallerySelectMode;
AnomalousBrowser.prototype.showGalleryViewer = showGalleryViewer;
AnomalousBrowser.prototype.showImageWorkbench = showImageWorkbench;
AnomalousBrowser.prototype.createDOM = createDOM;
AnomalousBrowser.prototype.openFolderManager = openFolderManager;
AnomalousBrowser.prototype.renderSidebar = renderSidebar;
AnomalousBrowser.prototype.loadFolders = loadFolders;
AnomalousBrowser.prototype.showHelp = showHelp;
AnomalousBrowser.prototype.hideAllPanels = hideAllPanels;
AnomalousBrowser.prototype.loadModels = loadModels;
AnomalousBrowser.prototype.applyModelToCanvas = applyModelToCanvas;
AnomalousBrowser.prototype.stopMediaInContainer = stopMediaInContainer;
AnomalousBrowser.prototype.showDetail = showDetail;
AnomalousBrowser.prototype.showEditModal = showEditModal;
AnomalousBrowser.prototype._openAdvancedModelSelector = _openAdvancedModelSelector;
AnomalousBrowser.prototype.setWidgetValuePath = setWidgetValuePath;
function syncFloatingTriggerVisibility() {
document.documentElement.classList.toggle('anomalous-topbar-entry-enabled', entryMode === 'topbar');
document.documentElement.classList.toggle('anomalous-floating-entry-enabled', entryMode === 'floating');
const browserIsOpen = browserInstance?.modal?.classList.contains('visible') === true;
const shouldShow = entryMode === 'floating' && !browserIsOpen;
triggerButton?.classList.toggle('anomalous-trigger-hidden', !shouldShow);
}
function applyFloatingTriggerPresentation() {
if (!triggerButton) return;
triggerButton.dataset.size = floatingTriggerSize;
triggerButton.dataset.style = floatingTriggerStyle;
const icon = document.createElement('span');
icon.className = 'anomalous-trigger-icon';
icon.setAttribute('aria-hidden', 'true');
icon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;display:block;"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
const label = document.createElement('span');
label.className = 'anomalous-trigger-label';
label.textContent = t('mainFloatingTriggerLabel');
triggerButton.replaceChildren(icon, label);
triggerButton.title = t('mainOpenTitle');
triggerButton.setAttribute('aria-label', t('mainOpenTitle'));
requestAnimationFrame(() => triggerBoundsUpdater?.());
}
function resetFloatingTriggerPosition() {
localStorage.removeItem('anomalous_btn_x');
localStorage.removeItem('anomalous_btn_y');
if (!triggerButton) return;
triggerButton.style.left = '';
triggerButton.style.top = '';
triggerButton.style.right = '30px';
triggerButton.style.bottom = '30px';
requestAnimationFrame(() => triggerBoundsUpdater?.());
}
function ensureBrowser() {
if (browserInstance) return browserInstance;
try {
browserInstance = new AnomalousBrowser();
browserInstance.triggerButton = triggerButton;
window.anomalousBrowserInstance = browserInstance;
triggerButton?.classList.remove('anomalous-trigger-error');
if (triggerButton) {
triggerButton.title = t('mainOpenTitle');
triggerButton.setAttribute('aria-label', t('mainOpenTitle'));
}
syncFloatingTriggerVisibility();
return browserInstance;
} catch (error) {
console.error('[Anomalous Model Browser] UI initialization failed:', error);
triggerButton?.classList.add('anomalous-trigger-error');
if (triggerButton) {
triggerButton.title = currentLang === 'zh' ? t('mainRetryInit') : t('mainRetryInitEn');
triggerButton.setAttribute('aria-label', triggerButton.title);
}
return null;
}
}
function openBrowser() {
ensureBrowser()?.show();
}
app.registerExtension({
name: 'Anomalous.ModelBrowser',
settings: [
{
id: FLOATING_TRIGGER_STYLE_SETTING_ID,
name: t('mainFloatingTriggerStyleSetting'),
category: ['Anomalous Model Browser', t('mainInterfaceCategory'), 'floating-trigger-style'],
tooltip: t('mainFloatingTriggerStyleTooltip'),
type: 'combo',
defaultValue: 'icon',
options: [
{ value: 'icon', text: t('mainFloatingTriggerStyleIcon') },
{ value: 'pill', text: t('mainFloatingTriggerStylePill') }
],
onChange(value) {
floatingTriggerStyle = normalizeFloatingTriggerStyle(value);
applyFloatingTriggerPresentation();
}
},
{
id: FLOATING_TRIGGER_SIZE_SETTING_ID,
name: t('mainFloatingTriggerSizeSetting'),
category: ['Anomalous Model Browser', t('mainInterfaceCategory'), 'floating-trigger-size'],
tooltip: t('mainFloatingTriggerSizeTooltip'),
type: 'combo',
defaultValue: 'medium',
options: [
{ value: 'small', text: t('mainFloatingTriggerSizeSmall') },
{ value: 'medium', text: t('mainFloatingTriggerSizeMedium') },
{ value: 'large', text: t('mainFloatingTriggerSizeLarge') }
],
onChange(value) {
floatingTriggerSize = normalizeFloatingTriggerSize(value);
applyFloatingTriggerPresentation();
}
},
{
id: SHORTCUT_SETTING_ID,
name: t('mainShortcutSetting'),
category: ['Anomalous Model Browser', t('mainInterfaceCategory'), 'shortcut'],
tooltip: t('mainShortcutTooltip'),
type: () => createShortcutSettingControl({
app,
commandId: OPEN_BROWSER_COMMAND_ID,
translate: t
}),
defaultValue: '',
telemetry: { trackChanges: false }
},
{
id: ENTRY_MODE_SETTING_ID,
name: t('mainEntryModeSetting'),
category: ['Anomalous Model Browser', t('mainInterfaceCategory'), 'entry-mode'],
tooltip: t('mainEntryModeTooltip'),
type: 'combo',
defaultValue: 'floating',
options: [
{ value: 'floating', text: t('mainEntryModeFloating') },
{ value: 'topbar', text: t('mainEntryModeTopbar') },
{ value: 'menu', text: t('mainEntryModeMenu') }
],
onChange(value) {
entryMode = normalizeEntryMode(value);
syncFloatingTriggerVisibility();
}
},
{
id: LANGUAGE_SETTING_ID,
name: t('mainLanguageSetting'),
category: ['Anomalous Model Browser', t('mainInterfaceCategory'), 'language'],
tooltip: t('mainLanguageTooltip'),
type: 'combo',
defaultValue: () => normalizeLanguagePreference(localStorage.getItem('anomalous_lang')),
options: [
{ value: 'auto', text: t('mainLanguageAuto') },
{ value: 'zh', text: t('mainLanguageChinese') },
{ value: 'en', text: t('mainLanguageEnglish') }
],
onChange(value) {
applyLanguagePreference(value);
}
},
{
id: ABYSSAL_SCARLET_SETTING_ID,
name: t('mainAbyssalScarletThemeSetting'),
category: ['Anomalous Model Browser', t('mainInterfaceCategory'), 'theme'],
tooltip: t('mainAbyssalScarletThemeTooltip'),
type: 'boolean',
defaultValue: () => localStorage.getItem('anomalous_theme_abyssal_scarlet') === 'true',
onChange(value) {
setAbyssalScarletTheme(Boolean(value), true);
}
}
],
actionBarButtons: [
{
icon: 'pi pi-box',
label: t('mainTopbarTriggerLabel'),
tooltip: t('mainOpenTitle'),
class: 'anomalous-topbar-entry',
onClick: openBrowser
}
],
commands: [
{
id: OPEN_BROWSER_COMMAND_ID,
label: t('mainOpenTitle'),
function: openBrowser
},
{
id: RESET_TRIGGER_POSITION_COMMAND_ID,
label: t('mainResetFloatingTriggerPosition'),
function: resetFloatingTriggerPosition
}
],
keybindings: [
{
combo: DEFAULT_BROWSER_SHORTCUT,
commandId: OPEN_BROWSER_COMMAND_ID
}
],
menuCommands: [
{
path: ['Extensions', 'Anomalous Model Browser'],
commands: [OPEN_BROWSER_COMMAND_ID, RESET_TRIGGER_POSITION_COMMAND_ID]
}
],
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);
}
const btn = document.createElement('button');
btn.id = 'anomalous-trigger-btn';
btn.setAttribute('aria-label', 'Anomalous Model Browser');
triggerButton = btn;
if (browserInstance) browserInstance.triggerButton = btn;
btn.title = t('mainOpenTitle');
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();
const nextPosition = clampFloatingTriggerPosition({
x: initialX + (e.clientX - startX),
y: initialY + (e.clientY - startY),
width: btn.offsetWidth,
height: btn.offsetHeight,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
margin: 0
});
btn.style.left = nextPosition.x + 'px';
btn.style.top = nextPosition.y + '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) {
openBrowser();
}
}
});
let savedX = localStorage.getItem('anomalous_btn_x');
let savedY = localStorage.getItem('anomalous_btn_y');
const updateBtnBounds = () => {
savedX = localStorage.getItem('anomalous_btn_x');
savedY = localStorage.getItem('anomalous_btn_y');
const position = clampFloatingTriggerPosition({
x: btn.style.left || savedX,
y: btn.style.top || savedY,
width: btn.offsetWidth,
height: btn.offsetHeight,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight
});
btn.style.left = position.x + 'px';
btn.style.top = position.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
};
triggerBoundsUpdater = updateBtnBounds;
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();
syncFloatingTriggerVisibility();
});
document.body.appendChild(btn);
try {
const settings = app.extensionManager?.setting;
const configuredEntryMode = settings?.get(ENTRY_MODE_SETTING_ID);
const configuredSize = settings?.get(FLOATING_TRIGGER_SIZE_SETTING_ID);
const configuredStyle = settings?.get(FLOATING_TRIGGER_STYLE_SETTING_ID);
entryMode = normalizeEntryMode(configuredEntryMode);
floatingTriggerSize = normalizeFloatingTriggerSize(configuredSize);
floatingTriggerStyle = normalizeFloatingTriggerStyle(configuredStyle);
} catch (error) {
console.warn('[Anomalous Model Browser] Unable to read entry preferences:', error);
}
applyFloatingTriggerPresentation();
syncFloatingTriggerVisibility();
// Initialize the shared browser instance once for every entry mode.
ensureBrowser();
// 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,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80'><rect width='76' height='76' x='2' y='2' fill='%23140812' fill-opacity='0.85' rx='16' stroke='%23f59e0b' stroke-width='2'/><text x='40' y='50' font-family='sans-serif' font-size='32' font-weight='bold' fill='%23f59e0b' text-anchor='middle'>W</text></svg>";
}
});
// --- 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 = `
<label style="display: block; margin-bottom: 8px; cursor: pointer;">
<input type="radio" name="amb-share-type" value="skeleton" checked />
${t('mainSkeletonOption')}
</label>
<label style="display: block; cursor: pointer;">
<input type="radio" name="amb-share-type" value="full" />
${t('mainFullOption')}
</label>
`;
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;