The rail's Prompts entry used to fold the browser away and open Prompt Studio in a drawer by the canvas, the one page that left the window. It is now the browser's Prompts page (owner.promptPanel): the rail, the 7 key, opening the browser with a text node selected, Current node's "Edit in Prompt Studio" and a kept prompt all go there, and reopening the browser returns to it. "Beside the canvas" on the page moves the studio into the drawer as before (side and width remembered, browser folded away); the drawer has "Back to the window" and Close. Only one studio is shown at a time. The page's studio stops when another page is shown, a workspace opens or the browser closes (its 400 ms poll with it) and is made again when the page is shown; closing a workspace opened from the Prompts page returns to it. Checked live on a test ComfyUI: a CLIP Text Encode opens the page aimed at it; docking, Back to the window and Close; 3 then 7; Workflows opened and closed from the page; reopening the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
238 lines
12 KiB
JavaScript
238 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 { leavePromptPage, 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 the Prompts page 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;
|
|
this.setTriggerVisible(false);
|
|
this.modal.classList.add('visible');
|
|
if (page === 'prompts') {
|
|
this.goTo('prompts');
|
|
return;
|
|
}
|
|
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() === 'prompts') this.goTo('prompts'); // its studio stopped when the browser closed
|
|
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?.();
|
|
leavePromptPage();
|
|
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;
|