From 4dc80e7f6e7aa333256b6147360cc25c4f5203f9 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 26 Aug 2025 09:40:17 +0800 Subject: [PATCH] feat: Implement sidebar navigation with folder tree and controls --- static/css/components/sidebar.css | 320 +++++++++++++++ static/css/layout.css | 104 +---- static/css/style.css | 2 +- static/js/components/SidebarManager.js | 377 ++++++++++++++++++ static/js/components/controls/PageControls.js | 179 ++------- static/js/loras.js | 2 - templates/components/controls.html | 23 +- templates/loras.html | 30 +- 8 files changed, 780 insertions(+), 257 deletions(-) create mode 100644 static/css/components/sidebar.css create mode 100644 static/js/components/SidebarManager.js diff --git a/static/css/components/sidebar.css b/static/css/components/sidebar.css new file mode 100644 index 00000000..ca57919b --- /dev/null +++ b/static/css/components/sidebar.css @@ -0,0 +1,320 @@ +/* Folder Sidebar Styles */ +.main-layout { + display: flex; + gap: 0; + width: 100%; + min-height: calc(100vh - 120px); +} + +.folder-sidebar { + width: 280px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-base); + overflow: hidden; + transition: all 0.3s ease; + flex-shrink: 0; + height: fit-content; + max-height: calc(100vh - 140px); + position: sticky; + top: 20px; +} + +.folder-sidebar.collapsed { + width: 0; + border: none; + opacity: 0; + pointer-events: none; +} + +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--lora-accent); + color: white; + font-weight: 600; + font-size: 0.9em; +} + +.sidebar-header h3 { + margin: 0; + font-size: 0.9em; + display: flex; + align-items: center; + gap: 8px; +} + +.sidebar-close-btn { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 4px; + border-radius: 4px; + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.sidebar-close-btn:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +.sidebar-content { + height: calc(100% - 45px); + overflow: hidden; +} + +.folder-tree-container { + height: 100%; + max-height: calc(100vh - 185px); + overflow-y: auto; + padding: 8px 0; +} + +/* Tree Node Styles */ +.sidebar-tree-node { + position: relative; + user-select: none; +} + +.sidebar-tree-node-content { + display: flex; + align-items: center; + padding: 6px 16px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.85em; + border-left: 3px solid transparent; +} + +.sidebar-tree-node-content:hover { + background: var(--lora-accent); + color: white; +} + +.sidebar-tree-node-content.selected { + background: var(--lora-accent); + color: white; + border-left-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.8); + font-weight: 500; +} + +.sidebar-tree-expand-icon { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 4px; + transition: transform 0.2s ease; + opacity: 0.6; +} + +.sidebar-tree-expand-icon.expanded { + transform: rotate(90deg); +} + +.sidebar-tree-expand-icon i { + font-size: 10px; +} + +.sidebar-tree-folder-icon { + margin-right: 8px; + color: var(--lora-accent); + opacity: 0.8; +} + +.sidebar-tree-node-content.selected .sidebar-tree-folder-icon { + color: white; + opacity: 1; +} + +.sidebar-tree-folder-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-tree-children { + overflow: hidden; + max-height: 0; + transition: max-height 0.3s ease; +} + +.sidebar-tree-children.expanded { + max-height: 1000px; +} + +.sidebar-tree-children .sidebar-tree-node-content { + padding-left: 32px; +} + +.sidebar-tree-children .sidebar-tree-children .sidebar-tree-node-content { + padding-left: 48px; +} + +.sidebar-tree-children .sidebar-tree-children .sidebar-tree-children .sidebar-tree-node-content { + padding-left: 64px; +} + +/* Breadcrumb Styles */ +.breadcrumb-container { + margin-top: 8px; + padding: 8px 0; + border-bottom: 1px solid var(--border-color); + background: var(--card-bg); + border-radius: var(--border-radius-xs); +} + +.breadcrumb-nav { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + font-size: 0.85em; + padding: 0 8px; +} + +.breadcrumb-item { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + cursor: pointer; + transition: all 0.2s ease; + color: var(--text-muted); +} + +.breadcrumb-item:hover { + background: var(--lora-accent); + color: white; +} + +.breadcrumb-item.active { + background: var(--lora-accent); + color: white; + font-weight: 500; +} + +.breadcrumb-separator { + color: var(--text-muted); + opacity: 0.6; + margin: 0 2px; +} + +/* Content Area */ +.content-area { + flex: 1; + min-width: 0; + margin-left: 16px; +} + +/* Sidebar Toggle Button */ +.sidebar-toggle-container { + margin-left: auto; +} + +.sidebar-toggle-btn { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--card-bg); + border: 1px solid var(--border-color); + color: var(--text-color); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.sidebar-toggle-btn:hover { + background: var(--lora-accent); + color: white; + transform: translateY(-2px); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); +} + +.sidebar-toggle-btn.active { + background: var(--lora-accent); + color: white; +} + +/* Empty State */ +.sidebar-tree-placeholder { + padding: 24px 16px; + text-align: center; + color: var(--text-muted); + opacity: 0.7; +} + +.sidebar-tree-placeholder i { + font-size: 2em; + opacity: 0.5; + margin-bottom: 8px; + display: block; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .folder-sidebar { + position: fixed; + top: 68px; + left: 16px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + max-height: calc(100vh - 88px); + } + + .folder-sidebar.collapsed { + left: -300px; + } + + .content-area { + margin-left: 0; + } + + .main-layout { + flex-direction: column; + } +} + +@media (max-width: 768px) { + .folder-sidebar { + width: calc(100vw - 32px); + left: 16px; + right: 16px; + } + + .breadcrumb-nav { + font-size: 0.8em; + } + + .breadcrumb-item { + padding: 3px 6px; + } +} + +/* Hide scrollbar but keep functionality */ +.folder-tree-container::-webkit-scrollbar { + width: 6px; +} + +.folder-tree-container::-webkit-scrollbar-track { + background: transparent; +} + +.folder-tree-container::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} + +.folder-tree-container::-webkit-scrollbar-thumb:hover { + background: var(--lora-accent); +} diff --git a/static/css/layout.css b/static/css/layout.css index 182e50b5..9a5776f4 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -225,38 +225,12 @@ display: none !important; } -.folder-tags-container { - position: relative; - width: 100%; - margin-bottom: 8px; /* Add margin to ensure space for the button */ -} - -.folder-tags { - display: flex; - gap: 4px; - padding: 2px 0; - flex-wrap: wrap; - transition: max-height 0.3s ease, opacity 0.2s ease; - max-height: 150px; /* Limit height to prevent overflow */ - opacity: 1; - overflow-y: auto; /* Enable vertical scrolling */ - margin-bottom: 5px; /* Add margin below the tags */ -} - -.folder-tags.collapsed { - max-height: 0; - opacity: 0; - margin: 0; - padding-bottom: 0; - overflow: hidden; -} - -.toggle-folders-container { +/* Sidebar Toggle Button */ +.sidebar-toggle-container { margin-left: auto; } -/* Toggle Folders Button */ -.toggle-folders-btn { +.sidebar-toggle-btn { width: 36px; height: 36px; border-radius: 50%; @@ -271,17 +245,13 @@ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } -.toggle-folders-btn:hover { +.sidebar-toggle-btn:hover { background: var(--lora-accent); color: white; transform: translateY(-2px); box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); } -.toggle-folders-btn i { - transition: transform 0.3s ease; -} - /* Icon-only button style */ .icon-only { min-width: unset !important; @@ -290,55 +260,6 @@ height: 32px !important; } -/* Rotate icon when folders are collapsed */ -.folder-tags.collapsed ~ .actions .toggle-folders-btn i { - transform: rotate(180deg); -} - -/* Add custom scrollbar for better visibility */ -.folder-tags::-webkit-scrollbar { - width: 6px; -} - -.folder-tags::-webkit-scrollbar-track { - background: var(--card-bg); - border-radius: 3px; -} - -.folder-tags::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; -} - -.folder-tags::-webkit-scrollbar-thumb:hover { - background: var(--lora-accent); -} - -.tag { - cursor: pointer; - padding: 2px 8px; - margin: 2px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - display: inline-block; - line-height: 1.2; - font-size: 14px; - background-color: var(--card-bg); - transition: all 0.2s ease; -} - -.tag:hover { - border-color: var(--lora-accent); - background-color: oklch(var(--lora-accent) / 0.1); - transform: translateY(-1px); -} - -.tag.active { - background-color: var(--lora-accent); - color: white; - border-color: var(--lora-accent); -} - /* Back to Top Button */ .back-to-top { position: fixed; @@ -376,10 +297,9 @@ } /* Prevent text selection in control and header areas */ -.tag, .control-group button, .control-group select, -.toggle-folders-btn, +.sidebar-toggle-btn, .bulk-operations-panel, .app-header, .header-branding, @@ -388,7 +308,7 @@ .nav-item, .header-actions button, .header-controls, -.toggle-folders-container button { +.sidebar-toggle-container button { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -473,15 +393,11 @@ margin-top: 8px; } - .toggle-folders-container { + .sidebar-toggle-container { margin-left: 0; } - .folder-tags-container { - order: -1; - } - - .toggle-folders-btn:hover { + .sidebar-toggle-btn:hover { transform: none; /* Disable hover effects on mobile */ } @@ -493,10 +409,6 @@ transform: none; /* Disable hover effects on mobile */ } - .tag:hover { - transform: none; /* Disable hover effects on mobile */ - } - .back-to-top { bottom: 60px; /* Give some extra space from bottom on mobile */ } diff --git a/static/css/style.css b/static/css/style.css index 0c581b3d..2e91f7c1 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -34,10 +34,10 @@ @import 'components/filter-indicator.css'; @import 'components/initialization.css'; @import 'components/progress-panel.css'; -@import 'components/alphabet-bar.css'; /* Add alphabet bar component */ @import 'components/duplicates.css'; /* Add duplicates component */ @import 'components/keyboard-nav.css'; /* Add keyboard navigation component */ @import 'components/statistics.css'; /* Add statistics component */ +@import 'components/sidebar.css'; /* Add sidebar component */ .initialization-notice { display: flex; diff --git a/static/js/components/SidebarManager.js b/static/js/components/SidebarManager.js new file mode 100644 index 00000000..e3aca45a --- /dev/null +++ b/static/js/components/SidebarManager.js @@ -0,0 +1,377 @@ +/** + * SidebarManager - Manages hierarchical folder navigation sidebar + */ +import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; +import { getModelApiClient } from '../api/modelApiFactory.js'; + +export class SidebarManager { + constructor(pageControls) { + this.pageControls = pageControls; + this.pageType = pageControls.pageType; + this.treeData = {}; + this.selectedPath = ''; + this.expandedNodes = new Set(); + this.isVisible = true; + this.apiClient = null; + + // Bind methods + this.handleTreeClick = this.handleTreeClick.bind(this); + this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this); + this.toggleSidebar = this.toggleSidebar.bind(this); + this.closeSidebar = this.closeSidebar.bind(this); + + this.init(); + } + + async init() { + this.apiClient = getModelApiClient(); + this.setupEventHandlers(); + this.restoreSidebarState(); + await this.loadFolderTree(); + this.restoreSelectedFolder(); + } + + setupEventHandlers() { + // Sidebar toggle button + const toggleBtn = document.querySelector('.sidebar-toggle-btn'); + if (toggleBtn) { + toggleBtn.addEventListener('click', this.toggleSidebar); + } + + // Sidebar close button + const closeBtn = document.getElementById('sidebarCloseBtn'); + if (closeBtn) { + closeBtn.addEventListener('click', this.closeSidebar); + } + + // Tree click handler + const folderTree = document.getElementById('sidebarFolderTree'); + if (folderTree) { + folderTree.addEventListener('click', this.handleTreeClick); + } + + // Breadcrumb click handler + const breadcrumbNav = document.getElementById('breadcrumbNav'); + if (breadcrumbNav) { + breadcrumbNav.addEventListener('click', this.handleBreadcrumbClick); + } + + // Close sidebar when clicking outside on mobile + document.addEventListener('click', (e) => { + if (window.innerWidth <= 1024) { + const sidebar = document.getElementById('folderSidebar'); + const toggleBtn = document.querySelector('.sidebar-toggle-btn'); + + if (sidebar && !sidebar.contains(e.target) && + toggleBtn && !toggleBtn.contains(e.target) && + !sidebar.classList.contains('collapsed')) { + this.closeSidebar(); + } + } + }); + + // Handle window resize + window.addEventListener('resize', () => { + if (window.innerWidth > 1024 && this.isVisible) { + const sidebar = document.getElementById('folderSidebar'); + if (sidebar) { + sidebar.classList.remove('collapsed'); + } + } + }); + } + + async loadFolderTree() { + try { + const response = await this.apiClient.fetchUnifiedFolderTree(); + this.treeData = response.tree || {}; + this.renderTree(); + } catch (error) { + console.error('Failed to load folder tree:', error); + this.renderEmptyState(); + } + } + + renderTree() { + const folderTree = document.getElementById('sidebarFolderTree'); + if (!folderTree) return; + + if (!this.treeData || Object.keys(this.treeData).length === 0) { + this.renderEmptyState(); + return; + } + + folderTree.innerHTML = this.renderTreeNode(this.treeData, ''); + } + + renderTreeNode(nodeData, basePath) { + const entries = Object.entries(nodeData); + if (entries.length === 0) return ''; + + return entries.map(([folderName, children]) => { + const currentPath = basePath ? `${basePath}/${folderName}` : folderName; + const hasChildren = Object.keys(children).length > 0; + const isExpanded = this.expandedNodes.has(currentPath); + const isSelected = this.selectedPath === currentPath; + + return ` +
+ `; + }).join(''); + } + + renderEmptyState() { + const folderTree = document.getElementById('sidebarFolderTree'); + if (!folderTree) return; + + folderTree.innerHTML = ` + + `; + } + + handleTreeClick(event) { + const expandIcon = event.target.closest('.sidebar-tree-expand-icon'); + const nodeContent = event.target.closest('.sidebar-tree-node-content'); + + if (expandIcon) { + // Toggle expand/collapse + const treeNode = expandIcon.closest('.sidebar-tree-node'); + const path = treeNode.dataset.path; + const children = treeNode.querySelector('.sidebar-tree-children'); + + if (this.expandedNodes.has(path)) { + this.expandedNodes.delete(path); + expandIcon.classList.remove('expanded'); + if (children) children.classList.remove('expanded'); + } else { + this.expandedNodes.add(path); + expandIcon.classList.add('expanded'); + if (children) children.classList.add('expanded'); + } + + this.saveExpandedState(); + } else if (nodeContent) { + // Select folder + const treeNode = nodeContent.closest('.sidebar-tree-node'); + const path = treeNode.dataset.path; + this.selectFolder(path); + } + } + + handleBreadcrumbClick(event) { + const breadcrumbItem = event.target.closest('.breadcrumb-item'); + if (breadcrumbItem) { + const path = breadcrumbItem.dataset.path || ''; + this.selectFolder(path); + } + } + + async selectFolder(path) { + // Update selected path + this.selectedPath = path; + + // Update UI + this.updateTreeSelection(); + this.updateBreadcrumbs(); + + // Update page state + this.pageControls.pageState.activeFolder = path || null; + setStorageItem(`${this.pageType}_activeFolder`, path || null); + + // Show/hide breadcrumb container + const breadcrumbContainer = document.getElementById('breadcrumbContainer'); + if (breadcrumbContainer) { + breadcrumbContainer.classList.toggle('hidden', !path); + } + + // Reload models with new filter + await this.pageControls.resetAndReload(); + + // Auto-close sidebar on mobile after selection + if (window.innerWidth <= 1024) { + this.closeSidebar(); + } + } + + updateTreeSelection() { + const folderTree = document.getElementById('sidebarFolderTree'); + if (!folderTree) return; + + // Remove all selections + folderTree.querySelectorAll('.sidebar-tree-node-content').forEach(node => { + node.classList.remove('selected'); + }); + + // Add selection to current path + if (this.selectedPath) { + const selectedNode = folderTree.querySelector(`[data-path="${this.selectedPath}"] .sidebar-tree-node-content`); + if (selectedNode) { + selectedNode.classList.add('selected'); + + // Expand parents to show selection + this.expandPathParents(this.selectedPath); + } + } + } + + expandPathParents(path) { + if (!path) return; + + const parts = path.split('/'); + let currentPath = ''; + + for (let i = 0; i < parts.length - 1; i++) { + currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]; + this.expandedNodes.add(currentPath); + } + + this.renderTree(); + } + + updateBreadcrumbs() { + const breadcrumbNav = document.getElementById('breadcrumbNav'); + if (!breadcrumbNav) return; + + const parts = this.selectedPath ? this.selectedPath.split('/') : []; + let currentPath = ''; + + const breadcrumbs = [` + + `]; + + parts.forEach((part, index) => { + currentPath = currentPath ? `${currentPath}/${part}` : part; + const isLast = index === parts.length - 1; + + breadcrumbs.push(``); + breadcrumbs.push(` + + `); + }); + + breadcrumbNav.innerHTML = breadcrumbs.join(''); + } + + toggleSidebar() { + const sidebar = document.getElementById('folderSidebar'); + const toggleBtn = document.querySelector('.sidebar-toggle-btn'); + + if (!sidebar) return; + + this.isVisible = !this.isVisible; + sidebar.classList.toggle('collapsed', !this.isVisible); + + if (toggleBtn) { + toggleBtn.classList.toggle('active', this.isVisible); + } + + this.saveSidebarState(); + } + + closeSidebar() { + const sidebar = document.getElementById('folderSidebar'); + const toggleBtn = document.querySelector('.sidebar-toggle-btn'); + + if (!sidebar) return; + + this.isVisible = false; + sidebar.classList.add('collapsed'); + + if (toggleBtn) { + toggleBtn.classList.remove('active'); + } + + this.saveSidebarState(); + } + + restoreSidebarState() { + const isVisible = getStorageItem(`${this.pageType}_sidebarVisible`, true); + const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []); + + this.isVisible = isVisible; + this.expandedNodes = new Set(expandedPaths); + + const sidebar = document.getElementById('folderSidebar'); + const toggleBtn = document.querySelector('.sidebar-toggle-btn'); + + if (sidebar) { + sidebar.classList.toggle('collapsed', !this.isVisible); + } + + if (toggleBtn) { + toggleBtn.classList.toggle('active', this.isVisible); + } + } + + restoreSelectedFolder() { + const activeFolder = getStorageItem(`${this.pageType}_activeFolder`); + if (activeFolder) { + this.selectedPath = activeFolder; + this.updateTreeSelection(); + this.updateBreadcrumbs(); + + // Show breadcrumb container + const breadcrumbContainer = document.getElementById('breadcrumbContainer'); + if (breadcrumbContainer) { + breadcrumbContainer.classList.remove('hidden'); + } + } + } + + saveSidebarState() { + setStorageItem(`${this.pageType}_sidebarVisible`, this.isVisible); + } + + saveExpandedState() { + setStorageItem(`${this.pageType}_expandedNodes`, Array.from(this.expandedNodes)); + } + + async refresh() { + await this.loadFolderTree(); + this.restoreSelectedFolder(); + } + + destroy() { + // Clean up event handlers + const toggleBtn = document.querySelector('.sidebar-toggle-btn'); + const closeBtn = document.getElementById('sidebarCloseBtn'); + const folderTree = document.getElementById('sidebarFolderTree'); + const breadcrumbNav = document.getElementById('breadcrumbNav'); + + if (toggleBtn) { + toggleBtn.removeEventListener('click', this.toggleSidebar); + } + if (closeBtn) { + closeBtn.removeEventListener('click', this.closeSidebar); + } + if (folderTree) { + folderTree.removeEventListener('click', this.handleTreeClick); + } + if (breadcrumbNav) { + breadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick); + } + } +} diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js index b39a8f57..60340231 100644 --- a/static/js/components/controls/PageControls.js +++ b/static/js/components/controls/PageControls.js @@ -2,6 +2,7 @@ import { getCurrentPageState, setCurrentPageType } from '../../state/index.js'; import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js'; import { showToast } from '../../utils/uiHelpers.js'; +import { SidebarManager } from '../SidebarManager.js'; /** * PageControls class - Unified control management for model pages @@ -23,6 +24,9 @@ export class PageControls { // Store API methods this.api = null; + // Initialize sidebar manager + this.sidebarManager = null; + // Initialize event listeners this.initEventListeners(); @@ -55,6 +59,21 @@ export class PageControls { registerAPI(api) { this.api = api; console.log(`API methods registered for ${this.pageType} page`); + + // Initialize sidebar manager after API is registered + this.initSidebarManager(); + } + + /** + * Initialize sidebar manager + */ + async initSidebarManager() { + try { + this.sidebarManager = new SidebarManager(this); + console.log('SidebarManager initialized'); + } catch (error) { + console.error('Failed to initialize SidebarManager:', error); + } } /** @@ -72,17 +91,6 @@ export class PageControls { }); } - // Use event delegation for folder tags - this is the key fix - const folderTagsContainer = document.querySelector('.folder-tags-container'); - if (folderTagsContainer) { - folderTagsContainer.addEventListener('click', (e) => { - const tag = e.target.closest('.tag'); - if (tag) { - this.handleFolderClick(tag); - } - }); - } - // Refresh button handler const refreshBtn = document.querySelector('[data-action="refresh"]'); if (refreshBtn) { @@ -92,12 +100,6 @@ export class PageControls { // Initialize dropdown functionality this.initDropdowns(); - // Toggle folders button - const toggleFoldersBtn = document.querySelector('.toggle-folders-btn'); - if (toggleFoldersBtn) { - toggleFoldersBtn.addEventListener('click', () => this.toggleFolderTags()); - } - // Clear custom filter handler const clearFilterBtn = document.querySelector('.clear-filter'); if (clearFilterBtn) { @@ -199,130 +201,6 @@ export class PageControls { } } - /** - * Toggle folder selection - * @param {HTMLElement} tagElement - The folder tag element that was clicked - */ - handleFolderClick(tagElement) { - const folder = tagElement.dataset.folder; - const wasActive = tagElement.classList.contains('active'); - - document.querySelectorAll('.folder-tags .tag').forEach(t => { - t.classList.remove('active'); - }); - - if (!wasActive) { - tagElement.classList.add('active'); - this.pageState.activeFolder = folder; - setStorageItem(`${this.pageType}_activeFolder`, folder); - } else { - this.pageState.activeFolder = null; - setStorageItem(`${this.pageType}_activeFolder`, null); - } - - this.resetAndReload(); - } - - /** - * Restore folder filter from storage - */ - restoreFolderFilter() { - const activeFolder = getStorageItem(`${this.pageType}_activeFolder`); - const folderTag = activeFolder && document.querySelector(`.tag[data-folder="${activeFolder}"]`); - - if (folderTag) { - folderTag.classList.add('active'); - this.pageState.activeFolder = activeFolder; - this.filterByFolder(activeFolder); - } - } - - /** - * Filter displayed cards by folder - * @param {string} folderPath - Folder path to filter by - */ - filterByFolder(folderPath) { - const cardSelector = this.pageType === 'loras' ? '.model-card' : '.checkpoint-card'; - document.querySelectorAll(cardSelector).forEach(card => { - card.style.display = card.dataset.folder === folderPath ? '' : 'none'; - }); - } - - /** - * Update the folder tags display with new folder list - * @param {Array} folders - List of folder names - */ - updateFolderTags(folders) { - const folderTagsContainer = document.querySelector('.folder-tags'); - if (!folderTagsContainer) return; - - // Keep track of currently selected folder - const currentFolder = this.pageState.activeFolder; - - // Create HTML for folder tags - const tagsHTML = folders.map(folder => { - const isActive = folder === currentFolder; - return `