Pages made at different times used their own greys (slate here, neutral there) and accents (sky, violet, indigo), and most ignored the Abyssal Scarlet theme. About 1,850 literal colours in web/styles and in CSS and inline styles written from JS now use the --amb-* tokens by what they are for: surfaces, text (new --amb-text-soft and --amb-text-dim), borders (new --amb-border-strong), the primary button, and one accent (new --amb-link, tinted with color-mix). Both themes define the new tokens. Literal colours stay where the colour is the meaning: status, prompt roles, voice emotions, canvas node colours, shadows and overlays; theme override rules are untouched. The share-code dialogs move from inline styles to .anomalous-share-* classes. Stylesheet versions are bumped. Checked: text contrast on every rail page, Prompt Studio, Prompt Notes, Model Sources, settings, model detail and editor, share codes, in both themes; structure check and JS tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
442 lines
22 KiB
JavaScript
442 lines
22 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';
|
|
|
|
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);
|
|
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.scanPanel = panel('anomalous-scan-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.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.scanPanel, this.doctorPanel,
|
|
this.assistantPanel, this.audioStudioPanel, 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 = '#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(255, 255, 255, 0.08)';
|
|
collapseAllBtn.style.color = '#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(255, 255, 255, 0.15)'; collapseAllBtn.style.borderColor = 'var(--amb-border-strong)'; };
|
|
collapseAllBtn.onmouseout = () => { collapseAllBtn.style.background = 'rgba(255, 255, 255, 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);
|
|
|
|
const searchBox = document.createElement('div');
|
|
searchBox.style.padding = '0 15px 15px 15px';
|
|
|
|
const searchInput = document.createElement('input');
|
|
searchInput.type = 'text';
|
|
searchInput.placeholder = t('sidebarSearchModels');
|
|
searchInput.style.width = '100%';
|
|
searchInput.style.padding = '8px 12px';
|
|
searchInput.style.borderRadius = '8px';
|
|
searchInput.style.border = '1px solid var(--amb-border)';
|
|
searchInput.style.background = 'rgba(0,0,0,0.2)';
|
|
searchInput.style.color = '#fff';
|
|
searchInput.style.boxSizing = 'border-box';
|
|
searchInput.style.outline = 'none';
|
|
searchInput.style.transition = 'border-color 0.2s';
|
|
searchInput.onfocus = () => searchInput.style.border = '1px solid #007aff';
|
|
searchInput.onblur = () => searchInput.style.border = '1px solid var(--amb-border)';
|
|
|
|
searchInput.oninput = (e) => {
|
|
if (this.currentDetailModel) {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel);
|
|
this.detailPanel.innerHTML = '';
|
|
this.currentDetailModel = null;
|
|
this.grid.style.display = 'grid';
|
|
}
|
|
const val = e.target.value.toLowerCase();
|
|
const cards = this.grid.querySelectorAll('.anomalous-card');
|
|
cards.forEach(card => {
|
|
const titleEl = card.querySelector('.anomalous-card-title');
|
|
if (!titleEl) return;
|
|
const titleText = titleEl.innerText.toLowerCase();
|
|
if (titleText.includes(val)) {
|
|
card.style.display = 'flex';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
};
|
|
|
|
searchBox.appendChild(searchInput);
|
|
this.sidebar.appendChild(searchBox);
|
|
|
|
(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="opacity:0.4; 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.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) { }
|
|
}
|