Add recursive search option for folder filtering and enhance search UI

This commit is contained in:
Will Miao
2025-02-08 18:01:22 +08:00
parent ad65974bb9
commit 25c93f8cc9
7 changed files with 118 additions and 11 deletions

View File

@@ -109,6 +109,7 @@ class ApiRoutes:
folder = request.query.get('folder') folder = request.query.get('folder')
search = request.query.get('search', '').lower() search = request.query.get('search', '').lower()
fuzzy = request.query.get('fuzzy', 'false').lower() == 'true' fuzzy = request.query.get('fuzzy', 'false').lower() == 'true'
recursive = request.query.get('recursive', 'false').lower() == 'true'
# Validate parameters # Validate parameters
if page < 1 or page_size < 1 or page_size > 100: if page < 1 or page_size < 1 or page_size > 100:
@@ -128,7 +129,8 @@ class ApiRoutes:
sort_by=sort_by, sort_by=sort_by,
folder=folder, folder=folder,
search=search, search=search,
fuzzy=fuzzy fuzzy=fuzzy,
recursive=recursive # 添加递归参数
) )
# Format the response data # Format the response data

View File

@@ -129,7 +129,19 @@ class LoraScanner:
return True return True
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'name', 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() cache = await self.get_cached_data()
# 先获取基础数据集 # 先获取基础数据集
@@ -137,9 +149,20 @@ class LoraScanner:
# 应用文件夹过滤 # 应用文件夹过滤
if folder is not None: 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 search:
if fuzzy: if fuzzy:
filtered_data = [ filtered_data = [

View File

@@ -847,6 +847,9 @@ body.modal-open {
width: 250px; width: 250px;
margin-left: auto; margin-left: auto;
flex-shrink: 0; /* 防止搜索框被压缩 */ flex-shrink: 0; /* 防止搜索框被压缩 */
display: flex;
align-items: center;
gap: 4px;
} }
/* 调整搜索框样式以匹配其他控件 */ /* 调整搜索框样式以匹配其他控件 */
@@ -869,7 +872,7 @@ body.modal-open {
.search-icon { .search-icon {
position: absolute; position: absolute;
right: 12px; right: calc(32px + 8px); /* 调整位置留出toggle按钮的空间 */
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: oklch(var(--text-color) / 0.5); color: oklch(var(--text-color) / 0.5);
@@ -877,6 +880,36 @@ body.modal-open {
line-height: 1; /* 防止图标影响容器高度 */ 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 { .corner-controls {
position: fixed; position: fixed;
top: 20px; top: 20px;
@@ -1049,7 +1082,7 @@ body.modal-open {
} }
.back-to-top:hover { .back-to-top:hover {
background: var(--lora-accent); background: var (--lora-accent);
color: white; color: white;
transform: translateY(-2px); transform: translateY(-2px);
} }

View File

@@ -15,8 +15,12 @@ export async function loadMoreLoras() {
sort_by: state.sortBy sort_by: state.sortBy
}); });
// 使用 state 中的 searchManager 获取递归搜索状态
const isRecursiveSearch = state.searchManager?.isRecursiveSearch ?? false;
if (state.activeFolder !== null) { if (state.activeFolder !== null) {
params.append('folder', state.activeFolder); params.append('folder', state.activeFolder);
params.append('recursive', isRecursiveSearch.toString());
} }
// Add search parameters if there's a search term // Add search parameters if there's a search term
@@ -251,4 +255,4 @@ export async function refreshLoras() {
state.loadingManager.hide(); state.loadingManager.hide();
state.loadingManager.restoreProgressBar(); state.loadingManager.restoreProgressBar();
} }
} }

View File

@@ -6,5 +6,6 @@ export const state = {
activeFolder: null, activeFolder: null,
loadingManager: null, loadingManager: null,
observer: null, observer: null,
previewVersions: new Map() previewVersions: new Map(),
}; searchManager: null // 添加 searchManager
};

View File

@@ -5,14 +5,52 @@ import { showToast } from './uiHelpers.js';
export class SearchManager { export class SearchManager {
constructor() { constructor() {
// Initialize search manager
this.searchInput = document.getElementById('searchInput'); this.searchInput = document.getElementById('searchInput');
this.searchModeToggle = document.getElementById('searchModeToggle');
this.searchDebounceTimeout = null; this.searchDebounceTimeout = null;
this.currentSearchTerm = ''; this.currentSearchTerm = '';
this.isSearching = false; this.isSearching = false;
this.isRecursiveSearch = false;
// Add this instance to state
state.searchManager = this;
if (this.searchInput) { if (this.searchInput) {
this.searchInput.addEventListener('input', this.handleSearch.bind(this)); 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) { handleSearch(event) {
@@ -53,8 +91,11 @@ export class SearchManager {
url.searchParams.set('search', searchTerm); url.searchParams.set('search', searchTerm);
url.searchParams.set('fuzzy', 'true'); url.searchParams.set('fuzzy', 'true');
// Always send folder parameter if there is an active folder
if (state.activeFolder) { if (state.activeFolder) {
url.searchParams.set('folder', 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); const response = await fetch(url);
@@ -85,4 +126,4 @@ export class SearchManager {
state.loadingManager.hide(); state.loadingManager.hide();
} }
} }
} }

View File

@@ -25,7 +25,10 @@
</div> </div>
<div class="search-container"> <div class="search-container">
<input type="text" id="searchInput" placeholder="Search models..." /> <input type="text" id="searchInput" placeholder="Search models..." />
<button class="search-mode-toggle" id="searchModeToggle" title="Toggle recursive search in folders">
<i class="fas fa-folder"></i>
</button>
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
</div> </div>
</div> </div>
</div> </div>