Opening the browser (the trigger, the action-bar button, the menu or its shortcut) now follows the node selected on the canvas: a node with prompt boxes and no model drop-down opens Prompt Studio, which targets that node; any other node (a KSampler, a model loader) opens Current node; nothing selected reopens the page used last. Settings -> Opening and window can turn it off. Prompt Studio closing, the doctor banner and MCP actions still open the browser as it was. While the browser is open, the plain keys 1-9 go to the rail's pages from the top. They are ignored in text fields, while an input method is composing and while a dialog is open; ComfyUI binds no plain digits (W is its Workflows key, so W/S were not used). The rail's tooltips name each page with its number. The rules are in shell_open_rules.js with tests/shell_open_rules.mjs. Checked live on a test ComfyUI: a CLIP Text Encode opens Prompt Studio on it, a KSampler opens Current node, 1/3/4/5/7/8 switch pages, digits typed into the search box stay there, and the setting turns the rule off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
236 lines
12 KiB
JavaScript
236 lines
12 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
import { showDetail } from './ui_detail.js';
|
|
import { showEditModal } from './ui_model_editor.js';
|
|
import { _openAdvancedModelSelector, setWidgetValuePath } from './ui_model_selector.js';
|
|
import { loadModels, applyModelToCanvas, stopMediaInContainer } from './ui_grid.js';
|
|
import { createDOM, renderSidebar, loadFolders } from './ui_sidebar.js';
|
|
import { closeWorkspace, hideAllPanels } from './ui_browser_navigation.js';
|
|
import { triggerDirectModelScan } from './scan_runner.js';
|
|
import { openScanPage, renderScanPage, leaveScanPage } from './ui_scan_page.js';
|
|
import { loadGalleryImages, refreshGalleryImages, showGeneratedGallery, showGallerySelectMode, showGalleryViewer } from './ui_gallery.js';
|
|
import { showNotebooks, openCombo, openComboByFilename, refreshNotebooks, saveCurrentNotebook, deleteCurrentNotebook } from './ui_notebooks.js';
|
|
import { renderNotebookEditor, fillNotebookGalleries } from './ui_notebook_editor.js';
|
|
import { sendNotebookToCanvas } from './notebook_canvas.js';
|
|
import { showRecipes, refreshRecipes } from './ui_recipe_catalog.js';
|
|
import { renderRecipeList, handleSaveRecipe, openRecipeByFilename } from './ui_recipes.js';
|
|
import { openPromptStudio } from './ui_prompt_composer.js';
|
|
import { closeUpdateGuide } from './ui_update_guide.js';
|
|
import { showImageWorkbench } from './ui_gallery_detail.js';
|
|
import { openDoctorPage } from './ui_doctor.js';
|
|
import { showModelSources } from './ui_model_sources.js';
|
|
import { initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, diagnoseNode, openCurrentNode, openLoraInsertionPicker } from './ui_node_assistant.js';
|
|
import { _openGalleryReplacer, findModelComboWidget } from './ui_node_model_picker.js';
|
|
import { promptBoxes } from './prompt_boxes.js';
|
|
import { followsSelection, pageForNode } from './shell_open_rules.js';
|
|
import { renderAudioStudio, stopAudioStudioPlayback } from './ui_audio_studio.js';
|
|
import { renderAudioGallery, stopGalleryAudio } from './ui_audio_gallery.js';
|
|
import { renderScriptPage } from './ui_script_page.js';
|
|
import { getActiveAudioFilter, setActiveAudioFilter, syncAudioSidebarSelection } from './ui_audio_sidebar.js';
|
|
import { voiceGroupKey } from './audio_engines.js';
|
|
import { getActiveDomain } from './ui_domain_switcher.js';
|
|
import { startPage } from './ui_shell_nav.js';
|
|
import { followComfyLanguage } from './interface_settings.js';
|
|
|
|
// What a selected node holds, for the page opening the browser shows (shell_open_rules.js).
|
|
const NODE_KINDS = {
|
|
takesPrompt: node => promptBoxes(node).length > 0,
|
|
hasModel: node => Boolean(findModelComboWidget(node)),
|
|
};
|
|
const selectedCanvasNode = () => Object.values(app.canvas?.selected_nodes || {})[0] || null;
|
|
|
|
export 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']);
|
|
// Video covers play on hover unless you chose "always": fewer videos decoding at once.
|
|
this.energySaving = localStorage.getItem('anomalous_energy_saving') !== 'false';
|
|
this.cardThumbnailMode = localStorage.getItem('anomalous_card_thumbnail_mode') === 'original'
|
|
? 'original'
|
|
: 'balanced';
|
|
this.entryMode = 'floating';
|
|
this.createDOM();
|
|
}
|
|
|
|
/**
|
|
* Opens the browser. `followSelection`: opened by the user (button, shortcut, menu), so a
|
|
* node selected on the canvas picks the page: a text node opens Prompt Studio on it, any
|
|
* other node Current node (Settings -> Opening and window can turn this off).
|
|
*/
|
|
show({ followSelection = false } = {}) {
|
|
followComfyLanguage();
|
|
if (this._idleReleaseTimer) {
|
|
clearTimeout(this._idleReleaseTimer);
|
|
this._idleReleaseTimer = null;
|
|
}
|
|
const page = followSelection && followsSelection() ? pageForNode(selectedCanvasNode(), NODE_KINDS) : null;
|
|
if (page === 'prompts') {
|
|
void this.openPromptStudio(); // beside the canvas, in place of the browser
|
|
return;
|
|
}
|
|
this.setTriggerVisible(false);
|
|
this.modal.classList.add('visible');
|
|
if (page === 'assistant') {
|
|
this.openCurrentNode();
|
|
return;
|
|
}
|
|
if (!this.currentShellPage()) {
|
|
this.goTo(startPage());
|
|
return;
|
|
}
|
|
// Opened again: stay on the page, refresh what may have changed meanwhile.
|
|
if (getActiveDomain() === 'audio') {
|
|
const audioTabs = { 'audio-gallery': 'gallery', script: 'script' };
|
|
this.switchAudioTab(audioTabs[this.currentShellPage()] || 'presets');
|
|
} else if (!this.foldersData) {
|
|
this.loadFolders();
|
|
} else {
|
|
this.loadModels();
|
|
}
|
|
if (this.currentShellPage() === 'gallery') void this.refreshGalleryImages();
|
|
if (this.currentShellPage() === 'scan') void renderScanPage(this, this.scanPanel);
|
|
}
|
|
|
|
/** The image / audio switch: the first page of that side. */
|
|
handleDomainChange(domain) {
|
|
this.goTo(domain === 'audio' ? 'voices' : 'models');
|
|
}
|
|
|
|
/** Single entry for audio-domain navigation: the rail, the audio list's entries and the domain switch. */
|
|
switchAudioTab(tabName, filter = null) {
|
|
if (!(tabName === 'presets' && filter?.type === 'group' && this.currentShellPage() === 'script')) this.hideAllPanels();
|
|
if (tabName === 'gallery') {
|
|
setActiveAudioFilter({ type: 'gallery', value: null });
|
|
this.markShellPage?.('audio-gallery');
|
|
this.audioGalleryPanel.style.display = 'block';
|
|
renderAudioGallery(this.audioGalleryPanel, { owner: this });
|
|
} else if (tabName === 'script') {
|
|
this.markShellPage?.('script');
|
|
this.scriptPanel.style.display = 'flex';
|
|
const pending = this.pendingScript || {};
|
|
this.pendingScript = null;
|
|
renderScriptPage(this.scriptPanel, this, pending);
|
|
} else if (filter?.type === 'group' && this.currentShellPage() === 'script') {
|
|
// A character picked in the list while on Voice-over: it speaks there.
|
|
this.openScript(filter.value);
|
|
return;
|
|
} else {
|
|
if (filter) setActiveAudioFilter(filter);
|
|
else if (getActiveAudioFilter().type === 'gallery') setActiveAudioFilter(null);
|
|
this.markShellPage?.('voices');
|
|
this.audioStudioPanel.style.display = 'block';
|
|
renderAudioStudio(this.audioStudioPanel, { owner: this });
|
|
}
|
|
syncAudioSidebarSelection(this);
|
|
}
|
|
|
|
/** One character's voice card: the Anomalous_TTS node's "import or edit characters". */
|
|
openVoice(character) {
|
|
const filter = character ? { type: 'group', value: voiceGroupKey(character), character } : null;
|
|
if (this.currentShellPage() === 'voices') {
|
|
this.switchAudioTab('presets', filter);
|
|
return;
|
|
}
|
|
if (filter) setActiveAudioFilter(filter);
|
|
this.goTo('voices'); // keeps the filter just set
|
|
}
|
|
|
|
/**
|
|
* The Voices page's Voice-over view. `group`: a voice group key to choose (a card, the list);
|
|
* `script`: `{ speech, subfolder }` of a generated file to voice again (the audio gallery).
|
|
*/
|
|
openScript(group = null, script = null) {
|
|
this.pendingScript = { character: group, script };
|
|
if (this.currentShellPage() === 'script') this.switchAudioTab('script');
|
|
else this.goTo('script');
|
|
}
|
|
|
|
close() {
|
|
this.flushCanvasActivity?.();
|
|
closeUpdateGuide(this);
|
|
leaveScanPage(); // a running scan's progress floats over the canvas
|
|
this.modal.classList.remove('visible');
|
|
this.setTriggerVisible(true);
|
|
stopAudioStudioPlayback();
|
|
stopGalleryAudio();
|
|
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 = [];
|
|
// The output gallery reloads its first page when opened again.
|
|
this.galleryGrid.querySelectorAll('.anomalous-gallery-card').forEach(card => card.remove());
|
|
this.galleryImagesList = [];
|
|
}, 90000);
|
|
}
|
|
|
|
setTriggerVisible(visible) {
|
|
const trigger = this.triggerButton || document.getElementById('anomalous-trigger-btn');
|
|
trigger?.classList.toggle('anomalous-trigger-hidden', !visible || this.entryMode !== 'floating');
|
|
}
|
|
}
|
|
|
|
AnomalousBrowser.prototype.openDoctorPage = function () { openDoctorPage(this); };
|
|
AnomalousBrowser.prototype.showModelSources = function (scope) { showModelSources(this, scope); };
|
|
AnomalousBrowser.prototype.diagnoseNode = diagnoseNode;
|
|
AnomalousBrowser.prototype.openCurrentNode = function () { openCurrentNode(this); };
|
|
AnomalousBrowser.prototype.initAssistantPanel = initAssistantPanel;
|
|
AnomalousBrowser.prototype.renderAssistantModelCard = renderAssistantModelCard;
|
|
AnomalousBrowser.prototype._loadAssistantHistory = _loadAssistantHistory;
|
|
AnomalousBrowser.prototype._openGalleryReplacer = _openGalleryReplacer;
|
|
AnomalousBrowser.prototype.openLoraInsertionPicker = openLoraInsertionPicker;
|
|
|
|
AnomalousBrowser.prototype.showNotebooks = showNotebooks;
|
|
AnomalousBrowser.prototype.openCombo = openCombo;
|
|
AnomalousBrowser.prototype.openComboByFilename = openComboByFilename;
|
|
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.openRecipeByFilename = openRecipeByFilename;
|
|
AnomalousBrowser.prototype.refreshRecipes = refreshRecipes;
|
|
AnomalousBrowser.prototype.renderRecipeList = renderRecipeList;
|
|
AnomalousBrowser.prototype.handleSaveRecipe = handleSaveRecipe;
|
|
AnomalousBrowser.prototype.openPromptStudio = openPromptStudio;
|
|
|
|
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.openScanPage = function (view) { openScanPage(this, view); };
|
|
AnomalousBrowser.prototype.scanSingleModel = triggerDirectModelScan;
|
|
AnomalousBrowser.prototype.renderSidebar = renderSidebar;
|
|
AnomalousBrowser.prototype.loadFolders = loadFolders;
|
|
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;
|