Optimize folder autocomplete with lazy loading and caching
All checks were successful
Docker Build / docker (push) Successful in 12s

- Lazy load folders only on input focus instead of page load
- Add debouncing (150ms) to reduce API calls during typing
- Implement in-memory caching for folder suggestions
- Add server-side ?q= filter parameter for faster searches
- Prevent concurrent duplicate folder fetch requests

Improves page load time and autocomplete responsiveness.
This commit is contained in:
2026-02-05 16:42:00 +07:00
parent 639d054a4f
commit 664eb7ddb9
4 changed files with 114 additions and 28 deletions

1
.gitignore vendored
View File

@@ -67,3 +67,4 @@ Thumbs.db
*.tmp
config.toml.example
.roo/memory
.kilocode/rules/*

View File

@@ -16,7 +16,6 @@ A Python-based application for managing anime with automated monitoring, notific
- [Features](#features)
- [Technology Stack](#technology-stack)
- [Architecture](#architecture)
- [Recent Updates](#recent-updates)
- [Project Structure](#project-structure)
- [Installation](#installation-1)
- [Docker Setup](#docker-setup)
@@ -113,17 +112,6 @@ graph TD
4. Results stored in database and notifications sent
5. Web dashboard provides real-time monitoring and management
## Recent Updates
- **Rule Management**: Added API endpoints and UI for creating, editing, and deleting regex rules
- **Progress Tracking**: New progress page showing in-progress file processing
- **Flush & Reprocess**: Ability to manually reprocess files after rule changes
- **Duplicate Episode Handling**: Improved processing logic to detect and handle duplicate episodes
- **Title Override**: Added ability to override titles in rules
- **Enhanced Error Logging**: Changed error logging to warnings for episode extraction and group index validation
- **Episode Extraction Fixes**: Improved group index validation and handling in extract_episode method
- **Command Parameter Handling**: Fixed command parameter handling for rule-specific regex index
## Project Structure
```

View File

@@ -6,6 +6,8 @@ import sqlite3
import toml
import shutil
from pathlib import Path
from functools import lru_cache
import hashlib
app = Flask(__name__)
@@ -337,9 +339,11 @@ def list_folders():
List folders for selection when creating rules.
Uses the configured media folder as the default root and
optionally allows browsing within it via a ?parent= query param.
Supports ?q= filter for searching folders by name.
"""
base_media = Path(config['general'].get('media_folder', '/'))
parent_param = request.args.get('parent')
search_query = request.args.get('q', '').lower()
# Determine which directory to list while ensuring we stay under media root
current_dir = base_media
@@ -359,6 +363,9 @@ def list_folders():
try:
for entry in sorted(current_dir.iterdir()):
if entry.is_dir():
# Apply search filter if provided
if search_query and search_query not in entry.name.lower():
continue
folders.append({
'name': entry.name,
'path': str(entry),

View File

@@ -313,8 +313,58 @@ function loadRegexHistory() {
});
}
// Populate folder suggestions for quick-add rule form
document.addEventListener('DOMContentLoaded', () => {
// Folder suggestions cache and debounce utility
const folderCache = new Map();
let folderFetchPromise = null;
let debounceTimer = null;
const DEBOUNCE_DELAY = 150; // ms
/**
* Debounced fetch with caching for folder suggestions
*/
function fetchFolderSuggestions(query = '') {
// Check cache first
const cacheKey = query;
if (folderCache.has(cacheKey)) {
return Promise.resolve(folderCache.get(cacheKey));
}
// Prevent duplicate concurrent requests
if (folderFetchPromise) {
return folderFetchPromise.then(result => {
// Return cached or filtered result
if (query === '') return result;
const filtered = result.filter(f =>
f.name.toLowerCase().includes(query.toLowerCase()) ||
f.path.toLowerCase().includes(query.toLowerCase())
);
folderCache.set(cacheKey, filtered);
return filtered;
});
}
folderFetchPromise = fetch(`/api/folders?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(data => {
folderFetchPromise = null;
if (data.error) return [];
const folders = data.folders || [];
folderCache.set(cacheKey, folders);
return folders;
})
.catch(err => {
folderFetchPromise = null;
console.error('Error fetching folders:', err);
return [];
});
return folderFetchPromise;
}
/**
* Populate datalist with folder options
*/
function populateFolderDatalist(folders) {
const datalistId = 'animeFolderOptionsDashboard';
let datalist = document.getElementById(datalistId);
if (!datalist) {
@@ -322,21 +372,61 @@ document.addEventListener('DOMContentLoaded', () => {
datalist.id = datalistId;
document.body.appendChild(datalist);
}
// Clear and repopulate
datalist.innerHTML = '';
folders.forEach(folder => {
const opt = document.createElement('option');
opt.value = folder.path;
opt.label = folder.name;
datalist.appendChild(opt);
});
}
fetch('/api/folders')
.then(r => r.json())
.then(data => {
if (!data.folders) return;
data.folders.forEach(folder => {
const opt = document.createElement('option');
opt.value = folder.path;
opt.textContent = folder.name;
datalist.appendChild(opt);
});
})
.catch(() => {
// Ignore errors; manual typing still works
});
/**
* Initialize folder autocomplete on input focus
*/
function initFolderAutocomplete() {
const input = document.querySelector('input[name="anime_folder"]');
if (!input) return;
let initialized = false;
input.addEventListener('focus', async () => {
if (initialized) return;
// Load initial suggestions on first focus
try {
const folders = await fetchFolderSuggestions();
populateFolderDatalist(folders);
initialized = true;
} catch (err) {
console.error('Failed to load folders:', err);
}
}, { once: true });
// Debounced search on input
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const query = input.value.trim();
if (query.length >= 2) {
// Search with query
const folders = await fetchFolderSuggestions(query);
populateFolderDatalist(folders);
} else if (initialized) {
// Reset to full list when query is short
const folders = await fetchFolderSuggestions();
populateFolderDatalist(folders);
}
}, DEBOUNCE_DELAY);
});
}
// Populate folder suggestions for quick-add rule form
document.addEventListener('DOMContentLoaded', () => {
// Initialize folder autocomplete (lazy loaded on focus)
initFolderAutocomplete();
// Initialize regex source UI
handleRegexSourceChange();