From 1800afe31ba17cbd52cd143c67a79fae1abb5b22 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 2 Sep 2025 15:48:34 +0800 Subject: [PATCH] feat(sidebar): implement display mode toggle and update sidebar actions for improved navigation. See #389 --- locales/de.json | 6 +- locales/en.json | 6 +- locales/es.json | 6 +- locales/fr.json | 6 +- locales/ja.json | 6 +- locales/ko.json | 6 +- locales/ru.json | 6 +- locales/zh-CN.json | 6 +- locales/zh-TW.json | 6 +- py/routes/base_model_routes.py | 8 +- static/css/components/sidebar.css | 59 ++++++++ static/js/api/baseModelApi.js | 2 + static/js/components/SidebarManager.js | 181 ++++++++++++++++++++--- templates/components/folder_sidebar.html | 5 +- 14 files changed, 272 insertions(+), 37 deletions(-) diff --git a/locales/de.json b/locales/de.json index 17b61a3b..874673c9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "Modell-Stammverzeichnis", "collapseAll": "Alle Ordner einklappen", - "pinToggle": "Seitenleiste anheften/lösen" + "pinSidebar": "Sidebar anheften", + "unpinSidebar": "Sidebar lösen", + "switchToListView": "Zur Listenansicht wechseln", + "switchToTreeView": "Zur Baumansicht wechseln", + "collapseAllDisabled": "Im Listenmodus nicht verfügbar" }, "statistics": { "title": "Statistiken", diff --git a/locales/en.json b/locales/en.json index 6045b5f3..fc609e15 100644 --- a/locales/en.json +++ b/locales/en.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "Model Root", "collapseAll": "Collapse All Folders", - "pinToggle": "Pin/Unpin Sidebar" + "pinSidebar": "Pin Sidebar", + "unpinSidebar": "Unpin Sidebar", + "switchToListView": "Switch to List View", + "switchToTreeView": "Switch to Tree View", + "collapseAllDisabled": "Not available in list view" }, "statistics": { "title": "Statistics", diff --git a/locales/es.json b/locales/es.json index 3eb14dab..9ca8a1aa 100644 --- a/locales/es.json +++ b/locales/es.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "Raíz del modelo", "collapseAll": "Colapsar todas las carpetas", - "pinToggle": "Anclar/Desanclar barra lateral" + "pinSidebar": "Fijar barra lateral", + "unpinSidebar": "Desfijar barra lateral", + "switchToListView": "Cambiar a vista de lista", + "switchToTreeView": "Cambiar a vista de árbol", + "collapseAllDisabled": "No disponible en vista de lista" }, "statistics": { "title": "Estadísticas", diff --git a/locales/fr.json b/locales/fr.json index 8137eb45..399a3fcf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "Racine du modèle", "collapseAll": "Réduire tous les dossiers", - "pinToggle": "Épingler/Désépingler la barre latérale" + "pinSidebar": "Épingler la barre latérale", + "unpinSidebar": "Désépingler la barre latérale", + "switchToListView": "Passer en vue liste", + "switchToTreeView": "Passer en vue arborescence", + "collapseAllDisabled": "Non disponible en vue liste" }, "statistics": { "title": "Statistiques", diff --git a/locales/ja.json b/locales/ja.json index 58cbc980..f531101e 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "モデルルート", "collapseAll": "すべてのフォルダを折りたたむ", - "pinToggle": "サイドバーの固定/固定解除" + "pinSidebar": "サイドバーを固定", + "unpinSidebar": "サイドバーの固定を解除", + "switchToListView": "リストビューに切り替え", + "switchToTreeView": "ツリービューに切り替え", + "collapseAllDisabled": "リストビューでは利用できません" }, "statistics": { "title": "統計", diff --git a/locales/ko.json b/locales/ko.json index efeda57f..2ea2fab2 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "모델 루트", "collapseAll": "모든 폴더 접기", - "pinToggle": "사이드바 고정/해제" + "pinSidebar": "사이드바 고정", + "unpinSidebar": "사이드바 고정 해제", + "switchToListView": "목록 보기로 전환", + "switchToTreeView": "트리 보기로 전환", + "collapseAllDisabled": "목록 보기에서는 사용할 수 없습니다" }, "statistics": { "title": "통계", diff --git a/locales/ru.json b/locales/ru.json index 2ee85271..d3d9eb96 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "Корень моделей", "collapseAll": "Свернуть все папки", - "pinToggle": "Закрепить/Открепить боковую панель" + "pinSidebar": "Закрепить боковую панель", + "unpinSidebar": "Открепить боковую панель", + "switchToListView": "Переключить на вид списка", + "switchToTreeView": "Переключить на древовидный вид", + "collapseAllDisabled": "Недоступно в виде списка" }, "statistics": { "title": "Статистика", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 04d7c5b1..28e2e551 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "模型根目录", "collapseAll": "折叠所有文件夹", - "pinToggle": "固定/取消固定侧边栏" + "pinSidebar": "固定侧边栏", + "unpinSidebar": "取消固定侧边栏", + "switchToListView": "切换到列表视图", + "switchToTreeView": "切换到树状视图", + "collapseAllDisabled": "列表视图下不可用" }, "statistics": { "title": "统计", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index ad970037..4b2b564f 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -400,7 +400,11 @@ "sidebar": { "modelRoot": "模型根目錄", "collapseAll": "全部摺疊資料夾", - "pinToggle": "釘選/取消釘選側邊欄" + "pinSidebar": "固定側邊欄", + "unpinSidebar": "取消固定側邊欄", + "switchToListView": "切換至列表檢視", + "switchToTreeView": "切換至樹狀檢視", + "collapseAllDisabled": "列表檢視下不可用" }, "statistics": { "title": "統計", diff --git a/py/routes/base_model_routes.py b/py/routes/base_model_routes.py index c870f4c0..bf77a9b2 100644 --- a/py/routes/base_model_routes.py +++ b/py/routes/base_model_routes.py @@ -114,18 +114,18 @@ class BaseModelRoutes(ABC): if not self.template_env or not template_name: return web.Response(text="Template environment or template name not set", status=500) - # 获取用户语言设置 + # Get user's language setting user_language = settings.get('language', 'en') - # 设置服务端i18n语言 + # Set server-side i18n locale server_i18n.set_locale(user_language) - # 为模板环境添加i18n过滤器 + # Add i18n filter to the template environment if not already added if not hasattr(self.template_env, '_i18n_filter_added'): self.template_env.filters['t'] = server_i18n.create_template_filter() self.template_env._i18n_filter_added = True - # 准备模板上下文 + # Prepare template context template_context = { 'is_initializing': is_initializing, 'settings': settings, diff --git a/static/css/components/sidebar.css b/static/css/components/sidebar.css index aacf57ae..67b417a8 100644 --- a/static/css/components/sidebar.css +++ b/static/css/components/sidebar.css @@ -132,6 +132,11 @@ color: var(--lora-accent); } +.sidebar-action-btn.disabled { + opacity: 0.3; + cursor: not-allowed; +} + /* Remove old close button styles */ .sidebar-toggle-close { display: none; @@ -365,6 +370,60 @@ font-weight: 500; } +/* Folder List Mode Styles */ +.sidebar-folder-item { + position: relative; + user-select: none; +} + +.sidebar-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-node-content:hover { + background: var(--lora-surface); + color: var(--text-color); +} + +.sidebar-folder-item.selected .sidebar-node-content { + 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-folder-icon { + margin-right: 8px; + color: var(--text-muted); + opacity: 0.7; + width: 16px; + text-align: center; +} + +.sidebar-folder-item.selected .sidebar-folder-icon { + color: var(--lora-accent); + opacity: 1; +} + +.sidebar-node-content:hover .sidebar-folder-icon { + color: var(--text-color); + opacity: 0.9; +} + +.sidebar-folder-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* Responsive Design */ @media (min-width: 2150px) { .folder-sidebar { diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 3c057f23..46e98a08 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -667,6 +667,8 @@ export class BaseModelApiClient { } } } + + params.append('recursive', pageState.searchOptions.recursive ? 'true' : 'false'); if (pageState.filters) { if (pageState.filters.tags && pageState.filters.tags.length > 0) { diff --git a/static/js/components/SidebarManager.js b/static/js/components/SidebarManager.js index 06ec52dd..1a46867c 100644 --- a/static/js/components/SidebarManager.js +++ b/static/js/components/SidebarManager.js @@ -3,6 +3,7 @@ */ import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; import { getModelApiClient } from '../api/modelApiFactory.js'; +import { translate } from '../utils/i18nHelpers.js'; export class SidebarManager { constructor() { @@ -18,6 +19,8 @@ export class SidebarManager { this.hoverTimeout = null; this.isHovering = false; this.isInitialized = false; + this.displayMode = 'tree'; // 'tree' or 'list' + this.foldersList = []; // Bind methods this.handleTreeClick = this.handleTreeClick.bind(this); @@ -31,6 +34,8 @@ export class SidebarManager { this.handleHoverAreaEnter = this.handleHoverAreaEnter.bind(this); this.handleHoverAreaLeave = this.handleHoverAreaLeave.bind(this); this.updateContainerMargin = this.updateContainerMargin.bind(this); + this.handleDisplayModeToggle = this.handleDisplayModeToggle.bind(this); + this.handleFolderListClick = this.handleFolderListClick.bind(this); } async initialize(pageControls) { @@ -105,6 +110,7 @@ export class SidebarManager { const sidebarHeader = document.getElementById('sidebarHeader'); const sidebar = document.getElementById('folderSidebar'); const hoverArea = document.getElementById('sidebarHoverArea'); + const displayModeToggleBtn = document.getElementById('sidebarDisplayModeToggle'); if (pinToggleBtn) { pinToggleBtn.removeEventListener('click', this.handlePinToggle); @@ -135,6 +141,10 @@ export class SidebarManager { // Remove resize event handler window.removeEventListener('resize', this.updateContainerMargin); + + if (displayModeToggleBtn) { + displayModeToggleBtn.removeEventListener('click', this.handleDisplayModeToggle); + } } async init() { @@ -258,6 +268,12 @@ export class SidebarManager { // Add dedicated resize listener for container margin updates window.addEventListener('resize', this.updateContainerMargin); + + // Display mode toggle button + const displayModeToggleBtn = document.getElementById('sidebarDisplayModeToggle'); + if (displayModeToggleBtn) { + displayModeToggleBtn.addEventListener('click', this.handleDisplayModeToggle); + } } handleDocumentClick(event) { @@ -270,7 +286,7 @@ export class SidebarManager { handleSidebarHeaderClick(event) { // Only trigger root selection if clicking on the title area, not the buttons if (!event.target.closest('.sidebar-header-actions')) { - this.selectFolder(''); + this.selectFolder(null); } } @@ -286,7 +302,7 @@ export class SidebarManager { handleCollapseAll(event) { event.stopPropagation(); this.expandedNodes.clear(); - this.renderTree(); + this.renderFolderDisplay(); this.saveExpandedState(); } @@ -407,21 +423,36 @@ export class SidebarManager { const pinBtn = document.getElementById('sidebarPinToggle'); if (pinBtn) { pinBtn.classList.toggle('active', this.isPinned); - pinBtn.title = this.isPinned ? 'Unpin Sidebar' : 'Pin Sidebar'; + pinBtn.title = this.isPinned + ? translate('sidebar.unpinSidebar') + : translate('sidebar.pinSidebar'); } } async loadFolderTree() { try { - const response = await this.apiClient.fetchUnifiedFolderTree(); - this.treeData = response.tree || {}; - this.renderTree(); + if (this.displayMode === 'tree') { + const response = await this.apiClient.fetchUnifiedFolderTree(); + this.treeData = response.tree || {}; + } else { + const response = await this.apiClient.fetchModelFolders(); + this.foldersList = response.folders || []; + } + this.renderFolderDisplay(); } catch (error) { - console.error('Failed to load folder tree:', error); + console.error('Failed to load folder data:', error); this.renderEmptyState(); } } + renderFolderDisplay() { + if (this.displayMode === 'tree') { + this.renderTree(); + } else { + this.renderFolderList(); + } + } + renderTree() { const folderTree = document.getElementById('sidebarFolderTree'); if (!folderTree) return; @@ -476,7 +507,38 @@ export class SidebarManager { `; } + renderFolderList() { + const folderTree = document.getElementById('sidebarFolderTree'); + if (!folderTree) return; + + if (!this.foldersList || this.foldersList.length === 0) { + this.renderEmptyState(); + return; + } + + const foldersHtml = this.foldersList.map(folder => { + const displayName = folder === '' ? '/' : folder; + const isSelected = this.selectedPath === folder; + + return ` +
+ `; + }).join(''); + + folderTree.innerHTML = foldersHtml; + } + handleTreeClick(event) { + if (this.displayMode === 'list') { + this.handleFolderListClick(event); + return; + } + const expandIcon = event.target.closest('.sidebar-tree-expand-icon'); const nodeContent = event.target.closest('.sidebar-tree-node-content'); @@ -516,7 +578,7 @@ export class SidebarManager { this.closeDropdown(); } else if (breadcrumbItem) { // Handle breadcrumb item click - const path = breadcrumbItem.dataset.path || ''; + const path = breadcrumbItem.dataset.path || null; // null for showing all models const isPlaceholder = breadcrumbItem.classList.contains('placeholder'); const isActive = breadcrumbItem.classList.contains('active'); const dropdown = breadcrumbItem.closest('.breadcrumb-dropdown'); @@ -557,8 +619,8 @@ export class SidebarManager { this.updateSidebarHeader(); // Update page state - this.pageControls.pageState.activeFolder = path || null; - setStorageItem(`${this.pageType}_activeFolder`, path || null); + this.pageControls.pageState.activeFolder = path; + setStorageItem(`${this.pageType}_activeFolder`, path); // Reload models with new filter await this.pageControls.resetAndReload(); @@ -569,23 +631,87 @@ export class SidebarManager { } } + handleFolderListClick(event) { + const folderItem = event.target.closest('.sidebar-folder-item'); + + if (folderItem) { + const path = folderItem.dataset.path; + this.selectFolder(path); + } + } + + handleDisplayModeToggle(event) { + event.stopPropagation(); + this.displayMode = this.displayMode === 'tree' ? 'list' : 'tree'; + this.updateDisplayModeButton(); + this.updateCollapseAllButton(); + this.updateSearchRecursiveOption(); + this.saveDisplayMode(); + this.loadFolderTree(); // Reload with new display mode + } + + updateDisplayModeButton() { + const displayModeBtn = document.getElementById('sidebarDisplayModeToggle'); + if (displayModeBtn) { + const icon = displayModeBtn.querySelector('i'); + if (this.displayMode === 'tree') { + icon.className = 'fas fa-sitemap'; + displayModeBtn.title = translate('sidebar.switchToListView'); + } else { + icon.className = 'fas fa-list'; + displayModeBtn.title = translate('sidebar.switchToTreeView'); + } + } + } + + updateCollapseAllButton() { + const collapseAllBtn = document.getElementById('sidebarCollapseAll'); + if (collapseAllBtn) { + if (this.displayMode === 'list') { + collapseAllBtn.disabled = true; + collapseAllBtn.classList.add('disabled'); + collapseAllBtn.title = translate('sidebar.collapseAllDisabled'); + } else { + collapseAllBtn.disabled = false; + collapseAllBtn.classList.remove('disabled'); + collapseAllBtn.title = translate('sidebar.collapseAll'); + } + } + } + + updateSearchRecursiveOption() { + this.pageControls.pageState.searchOptions.recursive = this.displayMode === 'tree'; + } + 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); + if (this.displayMode === 'list') { + // Remove all selections in list mode + folderTree.querySelectorAll('.sidebar-folder-item').forEach(item => { + item.classList.remove('selected'); + }); + + // Add selection to current path + if (this.selectedPath !== null) { + const selectedItem = folderTree.querySelector(`[data-path="${this.selectedPath}"]`); + if (selectedItem) { + selectedItem.classList.add('selected'); + } + } + } else { + // ...existing tree selection logic... + folderTree.querySelectorAll('.sidebar-tree-node-content').forEach(node => { + node.classList.remove('selected'); + }); + + if (this.selectedPath) { + const selectedNode = folderTree.querySelector(`[data-path="${this.selectedPath}"] .sidebar-tree-node-content`); + if (selectedNode) { + selectedNode.classList.add('selected'); + this.expandPathParents(this.selectedPath); + } } } } @@ -799,11 +925,16 @@ export class SidebarManager { restoreSidebarState() { const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false); const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []); + const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); this.isPinned = isPinned; this.expandedNodes = new Set(expandedPaths); + this.displayMode = displayMode; this.updatePinButton(); + this.updateDisplayModeButton(); + this.updateCollapseAllButton(); + this.updateSearchRecursiveOption(); } restoreSelectedFolder() { @@ -829,6 +960,10 @@ export class SidebarManager { setStorageItem(`${this.pageType}_expandedNodes`, Array.from(this.expandedNodes)); } + saveDisplayMode() { + setStorageItem(`${this.pageType}_displayMode`, this.displayMode); + } + async refresh() { await this.loadFolderTree(); this.restoreSelectedFolder(); diff --git a/templates/components/folder_sidebar.html b/templates/components/folder_sidebar.html index 6381c6ea..f5ea6c33 100644 --- a/templates/components/folder_sidebar.html +++ b/templates/components/folder_sidebar.html @@ -6,6 +6,9 @@