diff --git a/routes/api_routes.py b/routes/api_routes.py index beb78230..f8f92bbf 100644 --- a/routes/api_routes.py +++ b/routes/api_routes.py @@ -109,6 +109,7 @@ class ApiRoutes: folder = request.query.get('folder') search = request.query.get('search', '').lower() fuzzy = request.query.get('fuzzy', 'false').lower() == 'true' + recursive = request.query.get('recursive', 'false').lower() == 'true' # Validate parameters if page < 1 or page_size < 1 or page_size > 100: @@ -128,7 +129,8 @@ class ApiRoutes: sort_by=sort_by, folder=folder, search=search, - fuzzy=fuzzy + fuzzy=fuzzy, + recursive=recursive # 添加递归参数 ) # Format the response data diff --git a/services/lora_scanner.py b/services/lora_scanner.py index 12bf99ef..16cf4eab 100644 --- a/services/lora_scanner.py +++ b/services/lora_scanner.py @@ -129,7 +129,19 @@ class LoraScanner: return True async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'name', - folder: str = None, search: str = None, fuzzy: bool = False): + folder: str = None, search: str = None, fuzzy: bool = False, + recursive: bool = False): + """Get paginated and filtered lora data + + Args: + page: Current page number (1-based) + page_size: Number of items per page + sort_by: Sort method ('name' or 'date') + folder: Filter by folder path + search: Search term + fuzzy: Use fuzzy matching for search + recursive: Include subfolders when folder filter is applied + """ cache = await self.get_cached_data() # 先获取基础数据集 @@ -137,9 +149,20 @@ class LoraScanner: # 应用文件夹过滤 if folder is not None: - filtered_data = [item for item in filtered_data if item['folder'] == folder] + if recursive: + # 递归模式:匹配所有以该文件夹开头的路径 + filtered_data = [ + item for item in filtered_data + if item['folder'].startswith(folder + '/') or item['folder'] == folder + ] + else: + # 非递归模式:只匹配确切的文件夹 + filtered_data = [ + item for item in filtered_data + if item['folder'] == folder + ] - # 应用搜索过滤(只匹配model_name) + # 应用搜索过滤 if search: if fuzzy: filtered_data = [ diff --git a/static/css/style.css b/static/css/style.css index 8371970b..82b5b40e 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -847,6 +847,9 @@ body.modal-open { width: 250px; margin-left: auto; flex-shrink: 0; /* 防止搜索框被压缩 */ + display: flex; + align-items: center; + gap: 4px; } /* 调整搜索框样式以匹配其他控件 */ @@ -869,7 +872,7 @@ body.modal-open { .search-icon { position: absolute; - right: 12px; + right: calc(32px + 8px); /* 调整位置,留出toggle按钮的空间 */ top: 50%; transform: translateY(-50%); color: oklch(var(--text-color) / 0.5); @@ -877,6 +880,36 @@ body.modal-open { line-height: 1; /* 防止图标影响容器高度 */ } +.search-mode-toggle { + background: var(--lora-surface); + border: 1px solid oklch(65% 0.02 256); + border-radius: var(--border-radius-sm); + color: var(--text-color); + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.search-mode-toggle:hover { + background: var(--lora-accent); + color: white; +} + +.search-mode-toggle.active { + background: var(--lora-accent); + color: white; +} + +.search-mode-toggle i { + font-size: 0.9em; +} + .corner-controls { position: fixed; top: 20px; @@ -1049,7 +1082,7 @@ body.modal-open { } .back-to-top:hover { - background: var(--lora-accent); + background: var (--lora-accent); color: white; transform: translateY(-2px); } diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 801e0c68..457315df 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -15,8 +15,12 @@ export async function loadMoreLoras() { sort_by: state.sortBy }); + // 使用 state 中的 searchManager 获取递归搜索状态 + const isRecursiveSearch = state.searchManager?.isRecursiveSearch ?? false; + if (state.activeFolder !== null) { params.append('folder', state.activeFolder); + params.append('recursive', isRecursiveSearch.toString()); } // Add search parameters if there's a search term @@ -251,4 +255,4 @@ export async function refreshLoras() { state.loadingManager.hide(); state.loadingManager.restoreProgressBar(); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/static/js/state/index.js b/static/js/state/index.js index 3f396d75..0506fd97 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -6,5 +6,6 @@ export const state = { activeFolder: null, loadingManager: null, observer: null, - previewVersions: new Map() -}; \ No newline at end of file + previewVersions: new Map(), + searchManager: null // 添加 searchManager +}; \ No newline at end of file diff --git a/static/js/utils/search.js b/static/js/utils/search.js index 888ae68f..be02d92a 100644 --- a/static/js/utils/search.js +++ b/static/js/utils/search.js @@ -5,14 +5,52 @@ import { showToast } from './uiHelpers.js'; export class SearchManager { constructor() { + // Initialize search manager this.searchInput = document.getElementById('searchInput'); + this.searchModeToggle = document.getElementById('searchModeToggle'); this.searchDebounceTimeout = null; this.currentSearchTerm = ''; this.isSearching = false; + this.isRecursiveSearch = false; + + // Add this instance to state + state.searchManager = this; if (this.searchInput) { this.searchInput.addEventListener('input', this.handleSearch.bind(this)); } + + if (this.searchModeToggle) { + // Initialize toggle state from localStorage or default to false + this.isRecursiveSearch = localStorage.getItem('recursiveSearch') === 'true'; + this.updateToggleUI(); + + this.searchModeToggle.addEventListener('click', () => { + this.isRecursiveSearch = !this.isRecursiveSearch; + localStorage.setItem('recursiveSearch', this.isRecursiveSearch); + this.updateToggleUI(); + + // Rerun search if there's an active search term + if (this.currentSearchTerm) { + this.performSearch(this.currentSearchTerm); + } + }); + } + } + + updateToggleUI() { + if (this.searchModeToggle) { + this.searchModeToggle.classList.toggle('active', this.isRecursiveSearch); + this.searchModeToggle.title = this.isRecursiveSearch + ? 'Recursive folder search (including subfolders)' + : 'Current folder search only'; + + // Update the icon to indicate the mode + const icon = this.searchModeToggle.querySelector('i'); + if (icon) { + icon.className = this.isRecursiveSearch ? 'fas fa-folder-tree' : 'fas fa-folder'; + } + } } handleSearch(event) { @@ -53,8 +91,11 @@ export class SearchManager { url.searchParams.set('search', searchTerm); url.searchParams.set('fuzzy', 'true'); + // Always send folder parameter if there is an active folder if (state.activeFolder) { url.searchParams.set('folder', state.activeFolder); + // Add recursive parameter when recursive search is enabled + url.searchParams.set('recursive', this.isRecursiveSearch.toString()); } const response = await fetch(url); @@ -85,4 +126,4 @@ export class SearchManager { state.loadingManager.hide(); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/templates/components/controls.html b/templates/components/controls.html index bfeca41f..47ab3518 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -25,7 +25,10 @@