feat: Implement sidebar navigation with folder tree and controls

This commit is contained in:
Will Miao
2025-08-26 09:40:17 +08:00
parent c2a8508513
commit 4dc80e7f6e
8 changed files with 780 additions and 257 deletions

View File

@@ -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);
}

View File

@@ -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 */
}

View File

@@ -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;

View File

@@ -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 `
<div class="sidebar-tree-node" data-path="${currentPath}">
<div class="sidebar-tree-node-content ${isSelected ? 'selected' : ''}">
<div class="sidebar-tree-expand-icon ${isExpanded ? 'expanded' : ''}"
style="${hasChildren ? '' : 'opacity: 0; pointer-events: none;'}">
<i class="fas fa-chevron-right"></i>
</div>
<div class="sidebar-tree-folder-icon">
<i class="fas fa-folder"></i>
</div>
<div class="sidebar-tree-folder-name" title="${folderName}">${folderName}</div>
</div>
${hasChildren ? `
<div class="sidebar-tree-children ${isExpanded ? 'expanded' : ''}">
${this.renderTreeNode(children, currentPath)}
</div>
` : ''}
</div>
`;
}).join('');
}
renderEmptyState() {
const folderTree = document.getElementById('sidebarFolderTree');
if (!folderTree) return;
folderTree.innerHTML = `
<div class="sidebar-tree-placeholder">
<i class="fas fa-folder-open"></i>
<div>No folders found</div>
</div>
`;
}
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 = [`
<span class="breadcrumb-item ${!this.selectedPath ? 'active' : ''}" data-path="">
<i class="fas fa-home"></i> All Folders
</span>
`];
parts.forEach((part, index) => {
currentPath = currentPath ? `${currentPath}/${part}` : part;
const isLast = index === parts.length - 1;
breadcrumbs.push(`<span class="breadcrumb-separator">/</span>`);
breadcrumbs.push(`
<span class="breadcrumb-item ${isLast ? 'active' : ''}" data-path="${currentPath}">
${part}
</span>
`);
});
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);
}
}
}

View File

@@ -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 `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
}).join('');
// Update the container
folderTagsContainer.innerHTML = tagsHTML;
// Scroll active folder into view (no need to reattach click handlers)
const activeTag = folderTagsContainer.querySelector(`.tag[data-folder="${currentFolder}"]`);
if (activeTag) {
activeTag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
/**
* Toggle visibility of folder tags
*/
toggleFolderTags() {
const folderTags = document.querySelector('.folder-tags');
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (folderTags) {
folderTags.classList.toggle('collapsed');
if (folderTags.classList.contains('collapsed')) {
// Change icon to indicate folders are hidden
toggleBtn.className = 'fas fa-folder-plus';
toggleBtn.parentElement.title = 'Show folder tags';
setStorageItem('folderTagsCollapsed', 'true');
} else {
// Change icon to indicate folders are visible
toggleBtn.className = 'fas fa-folder-minus';
toggleBtn.parentElement.title = 'Hide folder tags';
setStorageItem('folderTagsCollapsed', 'false');
}
}
}
/**
* Initialize folder tags visibility based on stored preference
*/
initFolderTagsVisibility() {
const isCollapsed = getStorageItem('folderTagsCollapsed');
if (isCollapsed) {
const folderTags = document.querySelector('.folder-tags');
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (folderTags) {
folderTags.classList.add('collapsed');
}
if (toggleBtn) {
toggleBtn.className = 'fas fa-folder-plus';
toggleBtn.parentElement.title = 'Show folder tags';
}
} else {
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (toggleBtn) {
toggleBtn.className = 'fas fa-folder-minus';
toggleBtn.parentElement.title = 'Hide folder tags';
}
}
}
/**
* Load sort preference from storage
*/
@@ -408,6 +286,11 @@ export class PageControls {
try {
await this.api.resetAndReload(updateFolders);
// Refresh sidebar after reload if folders were updated
if (updateFolders && this.sidebarManager) {
await this.sidebarManager.refresh();
}
} catch (error) {
console.error(`Error reloading ${this.pageType}:`, error);
showToast(`Failed to reload ${this.pageType}: ${error.message}`, 'error');
@@ -426,6 +309,11 @@ export class PageControls {
try {
await this.api.refreshModels(fullRebuild);
// Refresh sidebar after rebuild
if (this.sidebarManager) {
await this.sidebarManager.refresh();
}
} catch (error) {
console.error(`Error ${fullRebuild ? 'rebuilding' : 'refreshing'} ${this.pageType}:`, error);
showToast(`Failed to ${fullRebuild ? 'rebuild' : 'refresh'} ${this.pageType}: ${error.message}`, 'error');
@@ -547,4 +435,13 @@ export class PageControls {
console.error('Model duplicates manager not available');
}
}
/**
* Clean up resources
*/
destroy() {
if (this.sidebarManager) {
this.sidebarManager.destroy();
}
}
}

View File

@@ -38,8 +38,6 @@ class LoraPageManager {
async initialize() {
// Initialize page-specific components
this.pageControls.restoreFolderFilter();
this.pageControls.initFolderTagsVisibility();
new LoraContextMenu();
// Initialize cards for current bulk mode state (should be false initially)

View File

@@ -1,14 +1,4 @@
<div class="controls">
{% if folders|length > 1 %}
<div class="folder-tags-container">
<div class="folder-tags">
{% for folder in folders %}
<div class="tag" data-folder="{{ folder }}">{{ folder }}</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="actions">
<div class="action-buttons">
<div title="Sort models by..." class="control-group">
@@ -75,9 +65,9 @@
</div>
<div class="controls-right">
<div class="toggle-folders-container">
<button class="toggle-folders-btn icon-only" title="Toggle folder tags">
<i class="fas fa-tags"></i>
<div class="sidebar-toggle-container">
<button class="sidebar-toggle-btn icon-only" title="Toggle folder sidebar">
<i class="fas fa-folder-tree"></i>
</button>
</div>
@@ -107,6 +97,13 @@
</div>
</div>
</div>
<!-- Breadcrumb Navigation -->
<div id="breadcrumbContainer" class="breadcrumb-container hidden">
<nav class="breadcrumb-nav" id="breadcrumbNav">
<!-- Breadcrumbs will be populated by JavaScript -->
</nav>
</div>
</div>
<!-- Add bulk operations panel (initially hidden) -->

View File

@@ -15,13 +15,35 @@
{% block content %}
{% include 'components/controls.html' %}
{% include 'components/alphabet_bar.html' %}
{% include 'components/duplicates_banner.html' %}
<!-- Lora卡片容器 -->
<div class="card-grid" id="modelGrid">
<!-- Cards will be dynamically inserted here -->
<div class="main-layout">
<!-- Folder Navigation Sidebar -->
<div class="folder-sidebar" id="folderSidebar">
<div class="sidebar-header">
<h3><i class="fas fa-folder-tree"></i> Folders</h3>
<button class="sidebar-close-btn" id="sidebarCloseBtn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sidebar-content">
<div class="folder-tree-container">
<div class="folder-tree" id="sidebarFolderTree">
<!-- Tree will be populated by JavaScript -->
</div>
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="content-area">
<!-- Lora卡片容器 -->
<div class="card-grid" id="modelGrid">
<!-- Cards will be dynamically inserted here -->
</div>
</div>
</div>
<!-- Bulk operations panel will be inserted here by JavaScript -->
{% endblock %}