Grey secondary and hint text is darker in the light theme. Labels and buttons laid over pictures (model card names, recipe cover labels, the gallery's View parameters button) keep the dark tokens, including ComfyUI's palette variables, so their text no longer turns dark on a dark backing. Prompt role badges, the red close crosses and the voice import button get colors that read in every theme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
406 lines
21 KiB
JavaScript
406 lines
21 KiB
JavaScript
/**
|
|
* ui_sidebar.js
|
|
* The browser window (rail, list column, header, page panels) and the model folder list.
|
|
*/
|
|
|
|
import { translate } from './locales.js';
|
|
import { escapeHtml } from './safe_dom.js';
|
|
import { showUpdateGuide } from './ui_update_guide.js';
|
|
import { AUDIO_USAGE_GUIDE, CURRENT_UPDATE_GUIDE } from './update_guide_data.js';
|
|
import { createSettingsHub } from './ui_settings_hub.js';
|
|
import { renderRailTools } from './ui_rail_tools.js';
|
|
import { getActiveDomain } from './ui_domain_switcher.js';
|
|
import { renderAudioSidebar } from './ui_audio_sidebar.js';
|
|
import { createGallerySearchBar } from './ui_gallery.js';
|
|
import { bindWorkspaceEscape } from './ui_browser_navigation.js';
|
|
import { createShellRail } from './ui_shell_rail.js';
|
|
import { installShellNavigation } from './ui_shell_nav.js';
|
|
import { bindShellDrag, bindShellResize } from './ui_shell_frame.js';
|
|
import { watchScans } from './ui_scan_watch.js';
|
|
import { watchCanvasChanges } from './activity_canvas.js';
|
|
import { watchWorkflowLoads } from './ui_doctor_banner.js';
|
|
import { settleModelScope } from './ui_model_types.js';
|
|
import { createModelSearch } from './ui_model_search.js';
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
const SIDEBAR_ICONS = {
|
|
DOCK: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>`,
|
|
LIST: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>`,
|
|
HELP: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-1px;margin-right:7px;flex-shrink:0;"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
SETTINGS: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
|
|
FOLDER: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-3px;margin-right:7px;"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>`,
|
|
CHEVRON_UP: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-2px;margin-right:4px;"><polyline points="18 15 12 9 6 15"/></svg>`,
|
|
CHEVRON_DOWN: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-2px;margin-right:4px;"><polyline points="6 9 12 15 18 9"/></svg>`
|
|
};
|
|
|
|
/** Five quick clicks on the rail's logo toggle the Abyssal Scarlet theme. */
|
|
function themeEasterEgg(badge) {
|
|
let clicks = 0;
|
|
let resetTimer = null;
|
|
return (e) => {
|
|
e.stopPropagation();
|
|
clicks++;
|
|
clearTimeout(resetTimer);
|
|
resetTimer = setTimeout(() => {
|
|
clicks = 0;
|
|
badge.removeAttribute('title');
|
|
}, 2500);
|
|
badge.style.transform = 'scale(0.92)';
|
|
setTimeout(() => { badge.style.transform = ''; }, 120);
|
|
if (clicks >= 5) {
|
|
clicks = 0;
|
|
badge.removeAttribute('title');
|
|
const active = document.documentElement.classList.contains('theme-abyssal-scarlet');
|
|
if (typeof window.setAbyssalScarletTheme === 'function') window.setAbyssalScarletTheme(!active, true);
|
|
} else if (clicks >= 2) {
|
|
badge.title = t('themeEasterEggHint', { count: clicks });
|
|
}
|
|
};
|
|
}
|
|
|
|
function headerButton(id, html, tooltip) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.id = id;
|
|
button.className = 'anomalous-tooltip-target';
|
|
button.innerHTML = html; // static markup only
|
|
button.setAttribute('aria-label', tooltip);
|
|
button.setAttribute('data-tooltip', tooltip);
|
|
button.setAttribute('data-tooltip-pos', 'bottom');
|
|
return button;
|
|
}
|
|
|
|
function panel(id, display = 'none') {
|
|
const node = document.createElement('div');
|
|
node.id = id;
|
|
node.style.display = display;
|
|
return node;
|
|
}
|
|
|
|
export function createDOM() {
|
|
localStorage.removeItem('anomalous_api_key');
|
|
localStorage.removeItem('anomalous_civitai_api_key');
|
|
this.modal = document.createElement('div');
|
|
this.modal.id = 'anomalous-modal';
|
|
|
|
const container = document.createElement('div');
|
|
container.id = 'anomalous-container';
|
|
container.classList.add('anomalous-has-rail');
|
|
|
|
const updateLangClass = () => {
|
|
container.classList.toggle('anomalous-lang-en', (window.anomalous_browser_lang || 'zh') === 'en');
|
|
};
|
|
updateLangClass();
|
|
|
|
const savedScale = localStorage.getItem('anomalous_ui_scale') || 1;
|
|
container.style.setProperty('--anomalous-scale', savedScale);
|
|
const savedBgOpacity = localStorage.getItem('anomalous_bg_opacity') || '0.2';
|
|
container.style.setProperty('--anomalous-bg-opacity', savedBgOpacity);
|
|
const savedViewMode = localStorage.getItem('anomalous_view_mode') || 'compact';
|
|
container.classList.add(`view-mode-${savedViewMode}`);
|
|
|
|
// Rail: pages, tools, settings. ui_rail_tools.js fills its tool slots below.
|
|
const rail = createShellRail(this);
|
|
const brand = rail.root.querySelector('.anomalous-rail-brand');
|
|
brand.addEventListener('click', themeEasterEgg(brand));
|
|
this.modelsBtn = rail.items.get('models');
|
|
this.galleryBtn = rail.items.get('gallery');
|
|
this.nbBtn = rail.items.get('recipes');
|
|
this.sidebarActions = this.railTools; // tools used to sit under the list
|
|
|
|
// List column: the model folders, or the characters on the audio pages.
|
|
this.sidebarWrapper = panel('anomalous-sidebar-wrapper', '');
|
|
this.sidebar = panel('anomalous-sidebar', '');
|
|
this.sidebarWrapper.appendChild(this.sidebar);
|
|
|
|
// Content: header (list button, page title, guide, dock, close) and the page panels.
|
|
const content = panel('anomalous-content', '');
|
|
const header = panel('anomalous-header', '');
|
|
bindShellDrag(container, header);
|
|
|
|
const leftGroup = document.createElement('div');
|
|
leftGroup.className = 'anomalous-header-group anomalous-header-left';
|
|
const centerGroup = document.createElement('div');
|
|
centerGroup.className = 'anomalous-header-group anomalous-header-center';
|
|
const rightGroup = document.createElement('div');
|
|
rightGroup.className = 'anomalous-header-group anomalous-header-right';
|
|
|
|
const listToggle = headerButton('anomalous-list-toggle', SIDEBAR_ICONS.LIST, t('sidebarToggle'));
|
|
const pageTitle = document.createElement('span');
|
|
pageTitle.className = 'anomalous-page-title';
|
|
leftGroup.append(listToggle, pageTitle);
|
|
|
|
const updateNoticeBtn = headerButton('anomalous-update-notice-btn', '', t('updateGuideNoticeTooltip'));
|
|
updateNoticeBtn.classList.add('anomalous-update-notice-btn');
|
|
const updateNoticeIcon = document.createElement('span');
|
|
updateNoticeIcon.className = 'anomalous-update-notice-icon';
|
|
updateNoticeIcon.setAttribute('aria-hidden', 'true');
|
|
updateNoticeIcon.textContent = '!';
|
|
updateNoticeBtn.appendChild(updateNoticeIcon);
|
|
// On the audio pages "!" explains the audio studio; elsewhere it shows what's new.
|
|
updateNoticeBtn.onclick = () => showUpdateGuide(this, {
|
|
force: true,
|
|
guide: getActiveDomain() === 'audio' ? AUDIO_USAGE_GUIDE : CURRENT_UPDATE_GUIDE,
|
|
});
|
|
|
|
const dockBtn = headerButton('anomalous-dock-btn', SIDEBAR_ICONS.DOCK, t('dockTitle'));
|
|
dockBtn.onclick = () => {
|
|
const docked = container.classList.toggle('anomalous-docked');
|
|
localStorage.setItem('anomalous_docked', docked ? 'true' : 'false');
|
|
};
|
|
if (localStorage.getItem('anomalous_docked') === 'true') container.classList.add('anomalous-docked');
|
|
|
|
const closeBtn = document.createElement('div');
|
|
closeBtn.id = 'anomalous-close';
|
|
closeBtn.innerHTML = '×';
|
|
closeBtn.onclick = () => this.close();
|
|
|
|
rightGroup.append(updateNoticeBtn, dockBtn, closeBtn);
|
|
centerGroup.append(createModelSearch(this));
|
|
header.append(leftGroup, centerGroup, rightGroup);
|
|
|
|
this.grid = panel('anomalous-grid', '');
|
|
this.detailPanel = panel('anomalous-detail');
|
|
this.galleryPanel = panel('anomalous-gallery-panel', '');
|
|
this.homePanel = panel('anomalous-home-panel');
|
|
this.activityPanel = panel('anomalous-activity-panel');
|
|
this.promptPanel = panel('anomalous-prompt-panel'); // Prompt Studio as a page (ui_prompt_composer.js)
|
|
this.scanPanel = panel('anomalous-scan-panel');
|
|
this.settingsPanel = panel('anomalous-settings-panel');
|
|
this.doctorPanel = panel('anomalous-doctor-panel');
|
|
this.assistantPanel = panel('anomalous-assistant-panel');
|
|
this.assistantPanel.style.flexDirection = 'column';
|
|
this.assistantPanel.style.flex = '1';
|
|
this.assistantPanel.style.overflowY = 'auto';
|
|
this.assistantPanel.style.boxSizing = 'border-box';
|
|
this.assistantPanelInitialized = false;
|
|
this.audioStudioPanel = panel('anomalous-audio-studio-panel');
|
|
this.audioStudioPanel.style.flex = '1';
|
|
this.audioStudioPanel.style.height = '100%';
|
|
this.audioStudioPanel.style.overflow = 'hidden';
|
|
this.audioStudioPanel.style.position = 'relative';
|
|
this.scriptPanel = panel('anomalous-script-panel');
|
|
this.audioGalleryPanel = panel('anomalous-audio-gallery-panel');
|
|
this.audioGalleryPanel.style.flex = '1';
|
|
this.audioGalleryPanel.style.height = '100%';
|
|
this.audioGalleryPanel.style.overflow = 'hidden';
|
|
|
|
this.galleryGrid = document.createElement('div');
|
|
this.galleryGrid.className = 'anomalous-gallery-grid';
|
|
this.galleryPanel.append(createGallerySearchBar(this), 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;
|
|
if (typeof IntersectionObserver === 'function') {
|
|
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);
|
|
} else {
|
|
// Some embedded ComfyUI webviews do not expose IntersectionObserver.
|
|
// The gallery still opens and loads its first page; do not abort the
|
|
// whole extension setup when infinite scroll is unavailable.
|
|
this.galleryObserver = null;
|
|
}
|
|
|
|
// Workspaces (recipes, notes, materials) cover the page but not the rail or header.
|
|
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.closeWorkspace();
|
|
};
|
|
bindWorkspaceEscape(this);
|
|
|
|
content.append(header, this.grid, this.detailPanel, this.galleryPanel, this.homePanel, this.activityPanel, this.promptPanel, this.scanPanel, this.settingsPanel, this.doctorPanel,
|
|
this.assistantPanel, this.audioStudioPanel, this.scriptPanel, this.audioGalleryPanel);
|
|
container.append(rail.root, this.sidebarWrapper, content, this.nbPanel);
|
|
this.modal.appendChild(container);
|
|
|
|
installShellNavigation(this, { container, rail, listToggle, title: pageTitle });
|
|
watchCanvasChanges(this);
|
|
watchWorkflowLoads(this);
|
|
|
|
const isScanning = watchScans(this);
|
|
const settingsHubControl = createSettingsHub(this, {
|
|
container,
|
|
savedScale,
|
|
savedBgOpacity,
|
|
updateLangClass,
|
|
dockBtn,
|
|
updateNoticeBtn,
|
|
icons: SIDEBAR_ICONS,
|
|
});
|
|
this.renderRailTools = () => renderRailTools(this, { isScanning, settingsButton: settingsHubControl.button });
|
|
this.renderRailTools();
|
|
|
|
bindShellResize(container);
|
|
document.body.appendChild(this.modal);
|
|
}
|
|
|
|
export function renderSidebar() {
|
|
if (getActiveDomain() === 'audio') {
|
|
renderAudioSidebar(this);
|
|
return;
|
|
}
|
|
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 = `${SIDEBAR_ICONS.FOLDER}<span>${t('folders')}</span>`;
|
|
title.style.color = 'var(--amb-lt-text, #fff)';
|
|
title.style.margin = '0';
|
|
title.style.display = 'inline-flex';
|
|
title.style.alignItems = 'center';
|
|
title.style.fontSize = '1.05em';
|
|
|
|
const isAllCollapsed = this.expandedFolders.size === 0;
|
|
const collapseAllBtn = document.createElement('button');
|
|
const collapseIcon = isAllCollapsed ? SIDEBAR_ICONS.CHEVRON_DOWN : SIDEBAR_ICONS.CHEVRON_UP;
|
|
collapseAllBtn.innerHTML = `${collapseIcon}<span>${t(isAllCollapsed ? 'sidebarExpandAll' : 'sidebarCollapseAll')}</span>`;
|
|
collapseAllBtn.style.display = 'inline-flex';
|
|
collapseAllBtn.style.alignItems = 'center';
|
|
collapseAllBtn.style.padding = '4px 9px';
|
|
collapseAllBtn.style.background = 'rgba(var(--amb-ink-rgb), 0.08)';
|
|
collapseAllBtn.style.color = 'var(--amb-lt-soft, #e2e8f0)';
|
|
collapseAllBtn.style.border = '1px solid var(--amb-border-strong)';
|
|
collapseAllBtn.style.borderRadius = '3px 8px 3px 8px';
|
|
collapseAllBtn.style.cursor = 'pointer';
|
|
collapseAllBtn.style.fontSize = '0.82em';
|
|
collapseAllBtn.style.transition = 'all 0.2s ease';
|
|
collapseAllBtn.onmouseover = () => { collapseAllBtn.style.background = 'rgba(var(--amb-ink-rgb), 0.15)'; collapseAllBtn.style.borderColor = 'var(--amb-border-strong)'; };
|
|
collapseAllBtn.onmouseout = () => { collapseAllBtn.style.background = 'rgba(var(--amb-ink-rgb), 0.08)'; collapseAllBtn.style.borderColor = 'var(--amb-border-strong)'; };
|
|
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);
|
|
|
|
|
|
(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 = `<span>${escapeHtml(typeGroup.label)}</span> <span>${isTypeExpanded ? '▼' : '▶'}</span>`;
|
|
|
|
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 = `<span class="anomalous-folder-toggle" style="margin-right: 8px; width: 12px; display: inline-block; font-size: 0.8em; color: var(--amb-text-muted);">${isExpanded ? '▼' : '▶'}</span>`;
|
|
} else {
|
|
toggleIcon = `<span style="margin-right: 8px; width: 12px; display: inline-block;"></span>`;
|
|
}
|
|
|
|
item.innerHTML = `${toggleIcon}<span class="anomalous-folder-name" style="color: var(--amb-text-soft);">${escapeHtml(info.name)}</span> <span style="color: var(--amb-text-dim); font-size:0.8em; margin-left: 5px;">${escapeHtml(info.model_count)}</span>`;
|
|
|
|
const scope = this.modelScope;
|
|
if (scope?.subfolder === path && scope.type === typeGroup.type && scope.path_idx === typeGroup.path_idx) {
|
|
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.modelScope = { type: typeGroup.type, path_idx: typeGroup.path_idx, subfolder: path };
|
|
this.modelView = '';
|
|
this.currentType = typeGroup.type;
|
|
this.currentPathIdx = typeGroup.path_idx;
|
|
this.currentSubfolder = path;
|
|
|
|
this.hideAllPanels();
|
|
this.grid.style.display = 'grid';
|
|
|
|
this.renderSidebar();
|
|
this.loadModels();
|
|
};
|
|
|
|
this.sidebar.appendChild(item);
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
|
|
export async function loadFolders() {
|
|
try {
|
|
const res = await fetch('/anomalous/folders');
|
|
const data = await res.json();
|
|
this.foldersData = data.folders || [];
|
|
settleModelScope(this);
|
|
|
|
// 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) { }
|
|
}
|