mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
@@ -191,7 +191,7 @@ class BaseModelRoutes(ABC):
|
||||
'modelname': request.query.get('search_modelname', 'true').lower() == 'true',
|
||||
'tags': request.query.get('search_tags', 'false').lower() == 'true',
|
||||
'creator': request.query.get('search_creator', 'false').lower() == 'true',
|
||||
'recursive': request.query.get('recursive', 'false').lower() == 'true',
|
||||
'recursive': request.query.get('recursive', 'true').lower() == 'true',
|
||||
}
|
||||
|
||||
# Parse hash filters if provided
|
||||
|
||||
@@ -68,7 +68,7 @@ class BaseModelService(ABC):
|
||||
'filename': True,
|
||||
'modelname': True,
|
||||
'tags': False,
|
||||
'recursive': False,
|
||||
'recursive': True,
|
||||
}
|
||||
|
||||
# Get the base data set using new sort logic
|
||||
@@ -139,7 +139,7 @@ class BaseModelService(ABC):
|
||||
|
||||
# Apply folder filtering
|
||||
if folder is not None:
|
||||
if search_options and search_options.get('recursive', False):
|
||||
if search_options and search_options.get('recursive', True):
|
||||
# Recursive folder filtering - include all subfolders
|
||||
data = [
|
||||
item for item in data
|
||||
|
||||
@@ -46,7 +46,7 @@ html, body {
|
||||
|
||||
/* Composed Colors */
|
||||
--lora-accent: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
|
||||
--lora-surface: oklch(100% 0 0 / 0.98);
|
||||
--lora-surface: oklch(97% 0 0 / 0.95);
|
||||
--lora-border: oklch(90% 0.02 256 / 0.15);
|
||||
--lora-text: oklch(95% 0.02 256);
|
||||
--lora-error: oklch(75% 0.32 29);
|
||||
|
||||
@@ -445,69 +445,6 @@
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
/* Switch styles */
|
||||
.search-option-switch {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 46px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px var(--lora-accent);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.search-options-panel,
|
||||
|
||||
495
static/css/components/sidebar.css
Normal file
495
static/css/components/sidebar.css
Normal file
@@ -0,0 +1,495 @@
|
||||
.folder-sidebar {
|
||||
position: fixed;
|
||||
top: 68px; /* Below header */
|
||||
left: 10px;
|
||||
width: 230px;
|
||||
height: calc(100vh - 88px);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
z-index: var(--z-overlay);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(8px);
|
||||
/* Default state: hidden off-screen */
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Visible state */
|
||||
.folder-sidebar.visible {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Auto-hide states */
|
||||
.folder-sidebar.auto-hide {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.folder-sidebar.auto-hide.hover-active {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.folder-sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hover detection area for auto-hide */
|
||||
.sidebar-hover-area {
|
||||
position: fixed;
|
||||
top: 68px;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: calc(100vh - 88px);
|
||||
z-index: calc(var(--z-overlay) - 1);
|
||||
background: transparent;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.sidebar-hover-area.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-header:hover {
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.sidebar-header.root-selected {
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar-action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar-action-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-action-btn.active {
|
||||
opacity: 1;
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15);
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
/* Remove old close button styles */
|
||||
.sidebar-toggle-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-tree-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Sidebar Tree Node Styles */
|
||||
.sidebar-tree-node {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar-tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.85em;
|
||||
border-left: 3px solid transparent;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-tree-node-content:hover {
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-tree-node-content.selected {
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
border-left-color: var(--lora-accent);
|
||||
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(--text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sidebar-tree-node-content.selected .sidebar-tree-folder-icon {
|
||||
color: var(--lora-accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-tree-node-content:hover .sidebar-tree-folder-icon {
|
||||
color: var(--text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Enhanced Sidebar Breadcrumb Styles */
|
||||
.sidebar-breadcrumb-container {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
font-size: 0.85em;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.sidebar-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);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-item:hover {
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-item.active {
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-separator {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
/* New Breadcrumb Dropdown Styles */
|
||||
.breadcrumb-dropdown {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-indicator {
|
||||
margin-left: 6px;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-item:hover .breadcrumb-dropdown-indicator {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-item.placeholder {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-item.placeholder:hover {
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown.open .breadcrumb-dropdown-indicator {
|
||||
transform: rotate(180deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 160px;
|
||||
max-width: 240px;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
|
||||
z-index: calc(var(--z-overlay) + 20);
|
||||
overflow-y: auto;
|
||||
max-height: 450px;
|
||||
display: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown.open .breadcrumb-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-item {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-item:hover {
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-item.active {
|
||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (min-width: 2000px) {
|
||||
.folder-sidebar {
|
||||
width: 280px;
|
||||
left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 3000px) {
|
||||
.folder-sidebar {
|
||||
width: 320px;
|
||||
left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.folder-sidebar {
|
||||
width: 260px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Smooth transitions for tree nodes */
|
||||
.sidebar-tree-node {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-tree-children {
|
||||
transition: max-height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sidebar-tree-expand-icon {
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Visual separator for nested levels */
|
||||
.sidebar-tree-children .sidebar-tree-node-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-tree-children .sidebar-tree-node-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--border-color);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.folder-sidebar {
|
||||
top: 68px;
|
||||
left: 16px;
|
||||
width: calc(100vw - 32px);
|
||||
max-width: 320px;
|
||||
height: calc(100vh - 88px);
|
||||
z-index: calc(var(--z-overlay) + 10);
|
||||
}
|
||||
|
||||
.folder-sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
/* Mobile overlay */
|
||||
.folder-sidebar:not(.collapsed)::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: -1;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.folder-sidebar {
|
||||
width: calc(100vw - 32px);
|
||||
max-width: 280px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-nav {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.sidebar-breadcrumb-item {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
.sidebar-tree-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar-tree-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-tree-container::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar-tree-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
@@ -225,63 +224,6 @@
|
||||
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 {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Toggle Folders Button */
|
||||
.toggle-folders-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);
|
||||
}
|
||||
|
||||
.toggle-folders-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 +232,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 +269,8 @@
|
||||
}
|
||||
|
||||
/* Prevent text selection in control and header areas */
|
||||
.tag,
|
||||
.control-group button,
|
||||
.control-group select,
|
||||
.toggle-folders-btn,
|
||||
.bulk-operations-panel,
|
||||
.app-header,
|
||||
.header-branding,
|
||||
@@ -387,8 +278,7 @@
|
||||
.main-nav,
|
||||
.nav-item,
|
||||
.header-actions button,
|
||||
.header-controls,
|
||||
.toggle-folders-container button {
|
||||
.header-controls {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
@@ -472,18 +362,6 @@
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.toggle-folders-container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.folder-tags-container {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.toggle-folders-btn:hover {
|
||||
transform: none; /* Disable hover effects on mobile */
|
||||
}
|
||||
|
||||
.control-group button:hover {
|
||||
transform: none; /* Disable hover effects on mobile */
|
||||
@@ -493,10 +371,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 */
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -671,7 +671,6 @@ export class BaseModelApiClient {
|
||||
if (pageState.searchOptions.creator !== undefined) {
|
||||
params.append('search_creator', pageState.searchOptions.creator.toString());
|
||||
}
|
||||
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,6 @@ class CheckpointsPageManager {
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Initialize page-specific components
|
||||
this.pageControls.restoreFolderFilter();
|
||||
this.pageControls.initFolderTagsVisibility();
|
||||
|
||||
// Initialize context menu
|
||||
new CheckpointContextMenu();
|
||||
|
||||
|
||||
729
static/js/components/SidebarManager.js
Normal file
729
static/js/components/SidebarManager.js
Normal file
@@ -0,0 +1,729 @@
|
||||
/**
|
||||
* 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.isPinned = false;
|
||||
this.apiClient = null;
|
||||
this.openDropdown = null;
|
||||
this.hoverTimeout = null;
|
||||
this.isHovering = false;
|
||||
|
||||
// Bind methods
|
||||
this.handleTreeClick = this.handleTreeClick.bind(this);
|
||||
this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this);
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
this.handleSidebarHeaderClick = this.handleSidebarHeaderClick.bind(this);
|
||||
this.handlePinToggle = this.handlePinToggle.bind(this);
|
||||
this.handleCollapseAll = this.handleCollapseAll.bind(this);
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
this.handleHoverAreaEnter = this.handleHoverAreaEnter.bind(this);
|
||||
this.handleHoverAreaLeave = this.handleHoverAreaLeave.bind(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.apiClient = getModelApiClient();
|
||||
|
||||
// Set initial sidebar state immediately (hidden by default)
|
||||
this.setInitialSidebarState();
|
||||
|
||||
this.setupEventHandlers();
|
||||
this.updateSidebarTitle();
|
||||
this.restoreSidebarState();
|
||||
await this.loadFolderTree();
|
||||
this.restoreSelectedFolder();
|
||||
|
||||
// Apply final state with animation after everything is loaded
|
||||
this.applyFinalSidebarState();
|
||||
}
|
||||
|
||||
setInitialSidebarState() {
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||
|
||||
if (!sidebar || !hoverArea) return;
|
||||
|
||||
// Get stored pin state
|
||||
const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false);
|
||||
this.isPinned = isPinned;
|
||||
|
||||
// Sidebar starts hidden by default (CSS handles this)
|
||||
// Just set up the hover area state
|
||||
if (window.innerWidth <= 1024) {
|
||||
hoverArea.classList.add('disabled');
|
||||
} else if (this.isPinned) {
|
||||
hoverArea.classList.add('disabled');
|
||||
} else {
|
||||
hoverArea.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
applyFinalSidebarState() {
|
||||
// Use requestAnimationFrame to ensure DOM is ready
|
||||
requestAnimationFrame(() => {
|
||||
this.updateAutoHideState();
|
||||
});
|
||||
}
|
||||
|
||||
updateSidebarTitle() {
|
||||
const sidebarTitle = document.getElementById('sidebarTitle');
|
||||
if (sidebarTitle) {
|
||||
sidebarTitle.textContent = `${this.apiClient.apiConfig.config.displayName} Root`;
|
||||
}
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
// Sidebar header (root selection) - only trigger on title area
|
||||
const sidebarHeader = document.getElementById('sidebarHeader');
|
||||
if (sidebarHeader) {
|
||||
sidebarHeader.addEventListener('click', this.handleSidebarHeaderClick);
|
||||
}
|
||||
|
||||
// Pin toggle button
|
||||
const pinToggleBtn = document.getElementById('sidebarPinToggle');
|
||||
if (pinToggleBtn) {
|
||||
pinToggleBtn.addEventListener('click', this.handlePinToggle);
|
||||
}
|
||||
|
||||
// Collapse all button
|
||||
const collapseAllBtn = document.getElementById('sidebarCollapseAll');
|
||||
if (collapseAllBtn) {
|
||||
collapseAllBtn.addEventListener('click', this.handleCollapseAll);
|
||||
}
|
||||
|
||||
// Tree click handler
|
||||
const folderTree = document.getElementById('sidebarFolderTree');
|
||||
if (folderTree) {
|
||||
folderTree.addEventListener('click', this.handleTreeClick);
|
||||
}
|
||||
|
||||
// Breadcrumb click handler
|
||||
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
|
||||
if (sidebarBreadcrumbNav) {
|
||||
sidebarBreadcrumbNav.addEventListener('click', this.handleBreadcrumbClick);
|
||||
}
|
||||
|
||||
// Hover detection for auto-hide
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||
|
||||
if (sidebar) {
|
||||
sidebar.addEventListener('mouseenter', this.handleMouseEnter);
|
||||
sidebar.addEventListener('mouseleave', this.handleMouseLeave);
|
||||
}
|
||||
|
||||
if (hoverArea) {
|
||||
hoverArea.addEventListener('mouseenter', this.handleHoverAreaEnter);
|
||||
hoverArea.addEventListener('mouseleave', this.handleHoverAreaLeave);
|
||||
}
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', (e) => {
|
||||
if (window.innerWidth <= 1024 && this.isVisible) {
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
|
||||
if (sidebar && !sidebar.contains(e.target)) {
|
||||
this.hideSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateAutoHideState();
|
||||
});
|
||||
|
||||
// Add document click handler for closing dropdowns
|
||||
document.addEventListener('click', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
handleDocumentClick(event) {
|
||||
// Close open dropdown when clicking outside
|
||||
if (this.openDropdown && !event.target.closest('.breadcrumb-dropdown')) {
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
handleSidebarHeaderClick(event) {
|
||||
// Only trigger root selection if clicking on the title area, not the buttons
|
||||
if (!event.target.closest('.sidebar-header-actions')) {
|
||||
this.selectFolder('');
|
||||
}
|
||||
}
|
||||
|
||||
handlePinToggle(event) {
|
||||
event.stopPropagation();
|
||||
this.isPinned = !this.isPinned;
|
||||
this.updateAutoHideState();
|
||||
this.updatePinButton();
|
||||
this.saveSidebarState();
|
||||
}
|
||||
|
||||
handleCollapseAll(event) {
|
||||
event.stopPropagation();
|
||||
this.expandedNodes.clear();
|
||||
this.renderTree();
|
||||
this.saveExpandedState();
|
||||
}
|
||||
|
||||
handleMouseEnter() {
|
||||
this.isHovering = true;
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
|
||||
if (!this.isPinned) {
|
||||
this.showSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave() {
|
||||
this.isHovering = false;
|
||||
if (!this.isPinned) {
|
||||
this.hoverTimeout = setTimeout(() => {
|
||||
if (!this.isHovering) {
|
||||
this.hideSidebar();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
handleHoverAreaEnter() {
|
||||
if (!this.isPinned) {
|
||||
this.showSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
handleHoverAreaLeave() {
|
||||
// Let the sidebar's mouse leave handler deal with hiding
|
||||
}
|
||||
|
||||
showSidebar() {
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
if (sidebar && !this.isPinned) {
|
||||
sidebar.classList.add('hover-active');
|
||||
this.isVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
hideSidebar() {
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
if (sidebar && !this.isPinned) {
|
||||
sidebar.classList.remove('hover-active');
|
||||
this.isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateAutoHideState() {
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||
|
||||
if (!sidebar || !hoverArea) return;
|
||||
|
||||
if (window.innerWidth <= 1024) {
|
||||
// Mobile: always use collapsed state
|
||||
sidebar.classList.remove('auto-hide', 'hover-active', 'visible');
|
||||
sidebar.classList.add('collapsed');
|
||||
hoverArea.classList.add('disabled');
|
||||
this.isVisible = false;
|
||||
} else if (this.isPinned) {
|
||||
// Desktop pinned: always visible
|
||||
sidebar.classList.remove('auto-hide', 'collapsed', 'hover-active');
|
||||
sidebar.classList.add('visible');
|
||||
hoverArea.classList.add('disabled');
|
||||
this.isVisible = true;
|
||||
} else {
|
||||
// Desktop auto-hide: use hover detection
|
||||
sidebar.classList.remove('collapsed', 'visible');
|
||||
sidebar.classList.add('auto-hide');
|
||||
hoverArea.classList.remove('disabled');
|
||||
|
||||
if (this.isHovering) {
|
||||
sidebar.classList.add('hover-active');
|
||||
this.isVisible = true;
|
||||
} else {
|
||||
sidebar.classList.remove('hover-active');
|
||||
this.isVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePinButton() {
|
||||
const pinBtn = document.getElementById('sidebarPinToggle');
|
||||
if (pinBtn) {
|
||||
pinBtn.classList.toggle('active', this.isPinned);
|
||||
pinBtn.title = this.isPinned ? 'Unpin Sidebar' : 'Pin Sidebar';
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<i class="fas fa-folder sidebar-tree-folder-icon"></i>
|
||||
<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('.sidebar-breadcrumb-item');
|
||||
const dropdownItem = event.target.closest('.breadcrumb-dropdown-item');
|
||||
|
||||
if (dropdownItem) {
|
||||
// Handle dropdown item selection
|
||||
const path = dropdownItem.dataset.path || '';
|
||||
this.selectFolder(path);
|
||||
this.closeDropdown();
|
||||
} else if (breadcrumbItem) {
|
||||
// Handle breadcrumb item click
|
||||
const path = breadcrumbItem.dataset.path || '';
|
||||
const isPlaceholder = breadcrumbItem.classList.contains('placeholder');
|
||||
const isActive = breadcrumbItem.classList.contains('active');
|
||||
const dropdown = breadcrumbItem.closest('.breadcrumb-dropdown');
|
||||
|
||||
if (isPlaceholder || (isActive && path === this.selectedPath)) {
|
||||
// Open dropdown for placeholders or active items
|
||||
// Close any open dropdown first
|
||||
if (this.openDropdown && this.openDropdown !== dropdown) {
|
||||
this.openDropdown.classList.remove('open');
|
||||
}
|
||||
|
||||
// Toggle current dropdown
|
||||
dropdown.classList.toggle('open');
|
||||
|
||||
// Update open dropdown reference
|
||||
this.openDropdown = dropdown.classList.contains('open') ? dropdown : null;
|
||||
} else {
|
||||
// Navigate to the selected path
|
||||
this.selectFolder(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async selectFolder(path) {
|
||||
// Update selected path
|
||||
this.selectedPath = path;
|
||||
|
||||
// Update UI
|
||||
this.updateTreeSelection();
|
||||
this.updateBreadcrumbs();
|
||||
this.updateSidebarHeader();
|
||||
|
||||
// Update page state
|
||||
this.pageControls.pageState.activeFolder = path || null;
|
||||
setStorageItem(`${this.pageType}_activeFolder`, path || null);
|
||||
|
||||
// Reload models with new filter
|
||||
await this.pageControls.resetAndReload();
|
||||
|
||||
// Auto-hide sidebar on mobile after selection
|
||||
if (window.innerWidth <= 1024) {
|
||||
this.hideSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Get sibling folders for a given path level
|
||||
getSiblingFolders(pathParts, level) {
|
||||
if (level === 0) {
|
||||
// Root level siblings are top-level folders
|
||||
return Object.keys(this.treeData);
|
||||
}
|
||||
|
||||
// Navigate to the parent folder to get siblings
|
||||
let currentNode = this.treeData;
|
||||
for (let i = 0; i < level; i++) {
|
||||
if (!currentNode[pathParts[i]]) {
|
||||
return [];
|
||||
}
|
||||
currentNode = currentNode[pathParts[i]];
|
||||
}
|
||||
|
||||
return Object.keys(currentNode);
|
||||
}
|
||||
|
||||
// Get child folders for a given path
|
||||
getChildFolders(path) {
|
||||
if (!path) {
|
||||
return Object.keys(this.treeData);
|
||||
}
|
||||
|
||||
const parts = path.split('/');
|
||||
let currentNode = this.treeData;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!currentNode[part]) {
|
||||
return [];
|
||||
}
|
||||
currentNode = currentNode[part];
|
||||
}
|
||||
|
||||
return Object.keys(currentNode);
|
||||
}
|
||||
|
||||
updateBreadcrumbs() {
|
||||
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
|
||||
if (!sidebarBreadcrumbNav) return;
|
||||
|
||||
const parts = this.selectedPath ? this.selectedPath.split('/') : [];
|
||||
let currentPath = '';
|
||||
|
||||
// Start with root breadcrumb
|
||||
const rootSiblings = Object.keys(this.treeData);
|
||||
const breadcrumbs = [`
|
||||
<div class="breadcrumb-dropdown">
|
||||
<span class="sidebar-breadcrumb-item ${!this.selectedPath ? 'active' : ''}" data-path="">
|
||||
<i class="fas fa-home"></i> ${this.apiClient.apiConfig.config.displayName} root
|
||||
</span>
|
||||
</div>
|
||||
`];
|
||||
|
||||
// Add separator and placeholder for next level if we're at root
|
||||
if (!this.selectedPath) {
|
||||
const nextLevelFolders = rootSiblings;
|
||||
if (nextLevelFolders.length > 0) {
|
||||
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
||||
breadcrumbs.push(`
|
||||
<div class="breadcrumb-dropdown">
|
||||
<span class="sidebar-breadcrumb-item placeholder">
|
||||
--
|
||||
<span class="breadcrumb-dropdown-indicator">
|
||||
<i class="fas fa-caret-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
<div class="breadcrumb-dropdown-menu">
|
||||
${nextLevelFolders.map(folder => `
|
||||
<div class="breadcrumb-dropdown-item" data-path="${folder}">
|
||||
${folder}
|
||||
</div>`).join('')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add breadcrumb items for each path segment
|
||||
parts.forEach((part, index) => {
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
const isLast = index === parts.length - 1;
|
||||
|
||||
// Get siblings for this level
|
||||
const siblings = this.getSiblingFolders(parts, index);
|
||||
|
||||
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
||||
breadcrumbs.push(`
|
||||
<div class="breadcrumb-dropdown">
|
||||
<span class="sidebar-breadcrumb-item ${isLast ? 'active' : ''}" data-path="${currentPath}">
|
||||
${part}
|
||||
${siblings.length > 1 ? `
|
||||
<span class="breadcrumb-dropdown-indicator">
|
||||
<i class="fas fa-caret-down"></i>
|
||||
</span>
|
||||
` : ''}
|
||||
</span>
|
||||
${siblings.length > 1 ? `
|
||||
<div class="breadcrumb-dropdown-menu">
|
||||
${siblings.map(folder => `
|
||||
<div class="breadcrumb-dropdown-item ${folder === part ? 'active' : ''}"
|
||||
data-path="${currentPath.replace(part, folder)}">
|
||||
${folder}
|
||||
</div>`).join('')
|
||||
}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Add separator and placeholder for next level if not the last item
|
||||
if (isLast) {
|
||||
const childFolders = this.getChildFolders(currentPath);
|
||||
if (childFolders.length > 0) {
|
||||
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
|
||||
breadcrumbs.push(`
|
||||
<div class="breadcrumb-dropdown">
|
||||
<span class="sidebar-breadcrumb-item placeholder">
|
||||
--
|
||||
<span class="breadcrumb-dropdown-indicator">
|
||||
<i class="fas fa-caret-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
<div class="breadcrumb-dropdown-menu">
|
||||
${childFolders.map(folder => `
|
||||
<div class="breadcrumb-dropdown-item" data-path="${currentPath}/${folder}">
|
||||
${folder}
|
||||
</div>`).join('')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sidebarBreadcrumbNav.innerHTML = breadcrumbs.join('');
|
||||
}
|
||||
|
||||
updateSidebarHeader() {
|
||||
const sidebarHeader = document.getElementById('sidebarHeader');
|
||||
if (!sidebarHeader) return;
|
||||
|
||||
if (!this.selectedPath) {
|
||||
sidebarHeader.classList.add('root-selected');
|
||||
} else {
|
||||
sidebarHeader.classList.remove('root-selected');
|
||||
}
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
this.isVisible = !this.isVisible;
|
||||
|
||||
if (this.isVisible) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
sidebar.classList.add('visible');
|
||||
} else {
|
||||
sidebar.classList.remove('visible');
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
|
||||
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.remove('visible');
|
||||
sidebar.classList.add('collapsed');
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
this.saveSidebarState();
|
||||
}
|
||||
|
||||
restoreSidebarState() {
|
||||
const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false);
|
||||
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
||||
|
||||
this.isPinned = isPinned;
|
||||
this.expandedNodes = new Set(expandedPaths);
|
||||
|
||||
this.updatePinButton();
|
||||
}
|
||||
|
||||
restoreSelectedFolder() {
|
||||
const activeFolder = getStorageItem(`${this.pageType}_activeFolder`);
|
||||
if (activeFolder) {
|
||||
this.selectedPath = activeFolder;
|
||||
this.updateTreeSelection();
|
||||
this.updateBreadcrumbs();
|
||||
this.updateSidebarHeader();
|
||||
} else {
|
||||
this.updateSidebarHeader();
|
||||
this.updateBreadcrumbs(); // Always update breadcrumbs
|
||||
}
|
||||
// Removed hidden class toggle since breadcrumbs are always visible now
|
||||
}
|
||||
|
||||
saveSidebarState() {
|
||||
setStorageItem(`${this.pageType}_sidebarPinned`, this.isPinned);
|
||||
}
|
||||
|
||||
saveExpandedState() {
|
||||
setStorageItem(`${this.pageType}_expandedNodes`, Array.from(this.expandedNodes));
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.loadFolderTree();
|
||||
this.restoreSelectedFolder();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Clear any pending timeouts
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
|
||||
// Clean up event handlers
|
||||
const pinToggleBtn = document.getElementById('sidebarPinToggle');
|
||||
const collapseAllBtn = document.getElementById('sidebarCollapseAll');
|
||||
const folderTree = document.getElementById('sidebarFolderTree');
|
||||
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
|
||||
const sidebarHeader = document.getElementById('sidebarHeader');
|
||||
const sidebar = document.getElementById('folderSidebar');
|
||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||
|
||||
if (pinToggleBtn) {
|
||||
pinToggleBtn.removeEventListener('click', this.handlePinToggle);
|
||||
}
|
||||
if (collapseAllBtn) {
|
||||
collapseAllBtn.removeEventListener('click', this.handleCollapseAll);
|
||||
}
|
||||
if (folderTree) {
|
||||
folderTree.removeEventListener('click', this.handleTreeClick);
|
||||
}
|
||||
if (sidebarBreadcrumbNav) {
|
||||
sidebarBreadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick);
|
||||
}
|
||||
if (sidebarHeader) {
|
||||
sidebarHeader.removeEventListener('click', this.handleSidebarHeaderClick);
|
||||
}
|
||||
if (sidebar) {
|
||||
sidebar.removeEventListener('mouseenter', this.handleMouseEnter);
|
||||
sidebar.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||
}
|
||||
if (hoverArea) {
|
||||
hoverArea.removeEventListener('mouseenter', this.handleHoverAreaEnter);
|
||||
hoverArea.removeEventListener('mouseleave', this.handleHoverAreaLeave);
|
||||
}
|
||||
|
||||
// Remove document click handler
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,6 @@ class EmbeddingsPageManager {
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Initialize page-specific components
|
||||
this.pageControls.restoreFolderFilter();
|
||||
this.pageControls.initFolderTagsVisibility();
|
||||
|
||||
// Initialize context menu
|
||||
new EmbeddingContextMenu();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,7 +19,6 @@ export class SearchManager {
|
||||
this.searchOptionsPanel = document.getElementById('searchOptionsPanel');
|
||||
this.closeSearchOptions = document.getElementById('closeSearchOptions');
|
||||
this.searchOptionTags = document.querySelectorAll('.search-option-tag');
|
||||
this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle');
|
||||
|
||||
this.searchTimeout = null;
|
||||
this.currentPage = options.page || document.body.dataset.page || 'loras';
|
||||
@@ -112,14 +111,6 @@ export class SearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Recursive search toggle
|
||||
if (this.recursiveSearchToggle) {
|
||||
this.recursiveSearchToggle.addEventListener('change', () => {
|
||||
this.saveSearchPreferences();
|
||||
this.performSearch();
|
||||
});
|
||||
}
|
||||
|
||||
// Add global click handler to close panels when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
// Close search options panel when clicking outside
|
||||
@@ -218,11 +209,6 @@ export class SearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Apply recursive search - only if the toggle exists
|
||||
if (this.recursiveSearchToggle && preferences.recursive !== undefined) {
|
||||
this.recursiveSearchToggle.checked = preferences.recursive;
|
||||
}
|
||||
|
||||
// Ensure at least one search option is selected
|
||||
this.validateSearchOptions();
|
||||
} catch (error) {
|
||||
@@ -272,11 +258,6 @@ export class SearchManager {
|
||||
options
|
||||
};
|
||||
|
||||
// Only add recursive option if the toggle exists
|
||||
if (this.recursiveSearchToggle) {
|
||||
preferences.recursive = this.recursiveSearchToggle.checked;
|
||||
}
|
||||
|
||||
setStorageItem(`${this.currentPage}_search_prefs`, preferences);
|
||||
} catch (error) {
|
||||
console.error('Error saving search preferences:', error);
|
||||
@@ -294,7 +275,6 @@ export class SearchManager {
|
||||
performSearch() {
|
||||
const query = this.searchInput.value.trim();
|
||||
const options = this.getActiveSearchOptions();
|
||||
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
|
||||
|
||||
// Update the state with search parameters
|
||||
const pageState = getCurrentPageState();
|
||||
@@ -318,16 +298,14 @@ export class SearchManager {
|
||||
filename: options.filename || false,
|
||||
modelname: options.modelname || false,
|
||||
tags: options.tags || false,
|
||||
creator: options.creator || false,
|
||||
recursive: recursive
|
||||
creator: options.creator || false
|
||||
};
|
||||
} else if (this.currentPage === 'checkpoints') {
|
||||
pageState.searchOptions = {
|
||||
filename: options.filename || false,
|
||||
modelname: options.modelname || false,
|
||||
tags: options.tags || false,
|
||||
creator: options.creator || false,
|
||||
recursive: recursive
|
||||
creator: options.creator || false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const state = {
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'name',
|
||||
activeFolder: null,
|
||||
activeFolder: getStorageItem(`${MODEL_TYPES.LORA}_activeFolder`),
|
||||
activeLetterFilter: null,
|
||||
previewVersions: loraPreviewVersions,
|
||||
searchManager: null,
|
||||
@@ -38,7 +38,7 @@ export const state = {
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
recursive: false
|
||||
recursive: true,
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
@@ -78,14 +78,14 @@ export const state = {
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'name',
|
||||
activeFolder: null,
|
||||
activeFolder: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_activeFolder`),
|
||||
previewVersions: checkpointPreviewVersions,
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
filename: true,
|
||||
modelname: true,
|
||||
creator: false,
|
||||
recursive: false
|
||||
recursive: true,
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
@@ -104,7 +104,7 @@ export const state = {
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'name',
|
||||
activeFolder: null,
|
||||
activeFolder: getStorageItem(`${MODEL_TYPES.EMBEDDING}_activeFolder`),
|
||||
activeLetterFilter: null,
|
||||
previewVersions: embeddingPreviewVersions,
|
||||
searchManager: null,
|
||||
@@ -113,7 +113,7 @@ export const state = {
|
||||
modelname: true,
|
||||
tags: false,
|
||||
creator: false,
|
||||
recursive: false
|
||||
recursive: true,
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
{% block content %}
|
||||
{% include 'components/controls.html' %}
|
||||
{% include 'components/duplicates_banner.html' %}
|
||||
{% include 'components/folder_sidebar.html' %}
|
||||
|
||||
<!-- Checkpoint cards container -->
|
||||
<div class="card-grid" id="modelGrid">
|
||||
@@ -38,6 +39,10 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block overlay %}
|
||||
<div class="bulk-mode-overlay"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_script %}
|
||||
<script type="module" src="/loras_static/js/checkpoints.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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,12 +65,6 @@
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="keyboard-nav-hint tooltip">
|
||||
<i class="fas fa-keyboard"></i>
|
||||
<span class="tooltiptext">
|
||||
@@ -107,6 +91,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<div id="breadcrumbContainer" class="sidebar-breadcrumb-container">
|
||||
<nav class="sidebar-breadcrumb-nav" id="sidebarBreadcrumbNav">
|
||||
<!-- Breadcrumbs will be populated by JavaScript -->
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add bulk operations panel (initially hidden) -->
|
||||
|
||||
24
templates/components/folder_sidebar.html
Normal file
24
templates/components/folder_sidebar.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- Hover detection area -->
|
||||
<div class="sidebar-hover-area" id="sidebarHoverArea"></div>
|
||||
|
||||
<!-- Folder Navigation Sidebar -->
|
||||
<div class="folder-sidebar" id="folderSidebar">
|
||||
<div class="sidebar-header" id="sidebarHeader">
|
||||
<h3><i class="fas fa-home"></i> <span id="sidebarTitle">Model Root</span></h3>
|
||||
<div class="sidebar-header-actions">
|
||||
<button class="sidebar-action-btn" id="sidebarCollapseAll" title="Collapse All Folders">
|
||||
<i class="fas fa-compress-alt"></i>
|
||||
</button>
|
||||
<button class="sidebar-action-btn" id="sidebarPinToggle" title="Pin/Unpin Sidebar">
|
||||
<i class="fas fa-thumbtack"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-tree-container">
|
||||
<div class="sidebar-tree" id="sidebarFolderTree">
|
||||
<!-- Tree will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,17 +101,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if request.path != '/loras/recipes' %}
|
||||
<div class="options-section">
|
||||
<div class="search-option-switch">
|
||||
<span>Include Subfolders</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="recursiveSearchToggle">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Add filter panel -->
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
{% block content %}
|
||||
{% include 'components/controls.html' %}
|
||||
{% include 'components/duplicates_banner.html' %}
|
||||
{% include 'components/folder_sidebar.html' %}
|
||||
|
||||
<!-- Embedding cards container -->
|
||||
<div class="card-grid" id="modelGrid">
|
||||
@@ -38,6 +39,10 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block overlay %}
|
||||
<div class="bulk-mode-overlay"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main_script %}
|
||||
<script type="module" src="/loras_static/js/embeddings.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
|
||||
{% block content %}
|
||||
{% include 'components/controls.html' %}
|
||||
{% include 'components/alphabet_bar.html' %}
|
||||
{% include 'components/duplicates_banner.html' %}
|
||||
{% include 'components/folder_sidebar.html' %}
|
||||
|
||||
<!-- Lora卡片容器 -->
|
||||
<div class="card-grid" id="modelGrid">
|
||||
<!-- Cards will be dynamically inserted here -->
|
||||
</div>
|
||||
|
||||
<!-- Bulk operations panel will be inserted here by JavaScript -->
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user