mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: implement alphabet filtering feature with letter counts and UI components v1
This commit is contained in:
@@ -69,6 +69,9 @@ class ApiRoutes:
|
|||||||
# Add the new trigger words route
|
# Add the new trigger words route
|
||||||
app.router.add_post('/loramanager/get_trigger_words', routes.get_trigger_words)
|
app.router.add_post('/loramanager/get_trigger_words', routes.get_trigger_words)
|
||||||
|
|
||||||
|
# Add new endpoint for letter counts
|
||||||
|
app.router.add_get('/api/loras/letter-counts', routes.get_letter_counts)
|
||||||
|
|
||||||
# Add update check routes
|
# Add update check routes
|
||||||
UpdateRoutes.setup_routes(app)
|
UpdateRoutes.setup_routes(app)
|
||||||
|
|
||||||
@@ -126,6 +129,9 @@ class ApiRoutes:
|
|||||||
tags = request.query.get('tags', None)
|
tags = request.query.get('tags', None)
|
||||||
favorites_only = request.query.get('favorites_only', 'false').lower() == 'true' # New parameter
|
favorites_only = request.query.get('favorites_only', 'false').lower() == 'true' # New parameter
|
||||||
|
|
||||||
|
# New parameter for alphabet filtering
|
||||||
|
first_letter = request.query.get('first_letter', None)
|
||||||
|
|
||||||
# New parameters for recipe filtering
|
# New parameters for recipe filtering
|
||||||
lora_hash = request.query.get('lora_hash', None)
|
lora_hash = request.query.get('lora_hash', None)
|
||||||
lora_hashes = request.query.get('lora_hashes', None)
|
lora_hashes = request.query.get('lora_hashes', None)
|
||||||
@@ -156,7 +162,8 @@ class ApiRoutes:
|
|||||||
tags=filters.get('tags', None),
|
tags=filters.get('tags', None),
|
||||||
search_options=search_options,
|
search_options=search_options,
|
||||||
hash_filters=hash_filters,
|
hash_filters=hash_filters,
|
||||||
favorites_only=favorites_only # Pass favorites_only parameter
|
favorites_only=favorites_only, # Pass favorites_only parameter
|
||||||
|
first_letter=first_letter # Pass the new first_letter parameter
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all available folders from cache
|
# Get all available folders from cache
|
||||||
@@ -1050,3 +1057,23 @@ class ApiRoutes:
|
|||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
async def get_letter_counts(self, request: web.Request) -> web.Response:
|
||||||
|
"""Get count of loras for each letter of the alphabet"""
|
||||||
|
try:
|
||||||
|
if self.scanner is None:
|
||||||
|
self.scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
|
|
||||||
|
# Get letter counts
|
||||||
|
letter_counts = await self.scanner.get_letter_counts()
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'letter_counts': letter_counts
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting letter counts: {e}")
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
from typing import List, Dict, Optional, Set
|
from typing import List, Dict, Optional, Set
|
||||||
|
|
||||||
from ..utils.models import LoraMetadata
|
from ..utils.models import LoraMetadata
|
||||||
@@ -123,7 +124,7 @@ class LoraScanner(ModelScanner):
|
|||||||
folder: str = None, search: str = None, fuzzy_search: bool = False,
|
folder: str = None, search: str = None, fuzzy_search: bool = False,
|
||||||
base_models: list = None, tags: list = None,
|
base_models: list = None, tags: list = None,
|
||||||
search_options: dict = None, hash_filters: dict = None,
|
search_options: dict = None, hash_filters: dict = None,
|
||||||
favorites_only: bool = False) -> Dict:
|
favorites_only: bool = False, first_letter: str = None) -> Dict:
|
||||||
"""Get paginated and filtered lora data
|
"""Get paginated and filtered lora data
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -138,6 +139,7 @@ class LoraScanner(ModelScanner):
|
|||||||
search_options: Dictionary with search options (filename, modelname, tags, recursive)
|
search_options: Dictionary with search options (filename, modelname, tags, recursive)
|
||||||
hash_filters: Dictionary with hash filtering options (single_hash or multiple_hashes)
|
hash_filters: Dictionary with hash filtering options (single_hash or multiple_hashes)
|
||||||
favorites_only: Filter for favorite models only
|
favorites_only: Filter for favorite models only
|
||||||
|
first_letter: Filter by first letter of model name
|
||||||
"""
|
"""
|
||||||
cache = await self.get_cached_data()
|
cache = await self.get_cached_data()
|
||||||
|
|
||||||
@@ -202,6 +204,10 @@ class LoraScanner(ModelScanner):
|
|||||||
lora for lora in filtered_data
|
lora for lora in filtered_data
|
||||||
if lora.get('favorite', False) is True
|
if lora.get('favorite', False) is True
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Apply first letter filtering
|
||||||
|
if first_letter:
|
||||||
|
filtered_data = self._filter_by_first_letter(filtered_data, first_letter)
|
||||||
|
|
||||||
# Apply folder filtering
|
# Apply folder filtering
|
||||||
if folder is not None:
|
if folder is not None:
|
||||||
@@ -273,6 +279,101 @@ class LoraScanner(ModelScanner):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _filter_by_first_letter(self, data, letter):
|
||||||
|
"""Filter data by first letter of model name
|
||||||
|
|
||||||
|
Special handling:
|
||||||
|
- '#': Numbers (0-9)
|
||||||
|
- '@': Special characters (not alphanumeric)
|
||||||
|
- '漢': CJK characters
|
||||||
|
"""
|
||||||
|
filtered_data = []
|
||||||
|
|
||||||
|
for lora in data:
|
||||||
|
model_name = lora.get('model_name', '')
|
||||||
|
if not model_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_char = model_name[0].upper()
|
||||||
|
|
||||||
|
if letter == '#' and first_char.isdigit():
|
||||||
|
filtered_data.append(lora)
|
||||||
|
elif letter == '@' and not first_char.isalnum():
|
||||||
|
# Special characters (not alphanumeric)
|
||||||
|
filtered_data.append(lora)
|
||||||
|
elif letter == '漢' and self._is_cjk_character(first_char):
|
||||||
|
# CJK characters
|
||||||
|
filtered_data.append(lora)
|
||||||
|
elif letter.upper() == first_char:
|
||||||
|
# Regular alphabet matching
|
||||||
|
filtered_data.append(lora)
|
||||||
|
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
|
def _is_cjk_character(self, char):
|
||||||
|
"""Check if character is a CJK character"""
|
||||||
|
# Define Unicode ranges for CJK characters
|
||||||
|
cjk_ranges = [
|
||||||
|
(0x4E00, 0x9FFF), # CJK Unified Ideographs
|
||||||
|
(0x3400, 0x4DBF), # CJK Unified Ideographs Extension A
|
||||||
|
(0x20000, 0x2A6DF), # CJK Unified Ideographs Extension B
|
||||||
|
(0x2A700, 0x2B73F), # CJK Unified Ideographs Extension C
|
||||||
|
(0x2B740, 0x2B81F), # CJK Unified Ideographs Extension D
|
||||||
|
(0x2B820, 0x2CEAF), # CJK Unified Ideographs Extension E
|
||||||
|
(0x2CEB0, 0x2EBEF), # CJK Unified Ideographs Extension F
|
||||||
|
(0x30000, 0x3134F), # CJK Unified Ideographs Extension G
|
||||||
|
(0xF900, 0xFAFF), # CJK Compatibility Ideographs
|
||||||
|
(0x3300, 0x33FF), # CJK Compatibility
|
||||||
|
(0x3200, 0x32FF), # Enclosed CJK Letters and Months
|
||||||
|
(0x3100, 0x312F), # Bopomofo
|
||||||
|
(0x31A0, 0x31BF), # Bopomofo Extended
|
||||||
|
(0x3040, 0x309F), # Hiragana
|
||||||
|
(0x30A0, 0x30FF), # Katakana
|
||||||
|
(0x31F0, 0x31FF), # Katakana Phonetic Extensions
|
||||||
|
(0xAC00, 0xD7AF), # Hangul Syllables
|
||||||
|
(0x1100, 0x11FF), # Hangul Jamo
|
||||||
|
(0xA960, 0xA97F), # Hangul Jamo Extended-A
|
||||||
|
(0xD7B0, 0xD7FF), # Hangul Jamo Extended-B
|
||||||
|
]
|
||||||
|
|
||||||
|
code_point = ord(char)
|
||||||
|
return any(start <= code_point <= end for start, end in cjk_ranges)
|
||||||
|
|
||||||
|
async def get_letter_counts(self):
|
||||||
|
"""Get count of models for each letter of the alphabet"""
|
||||||
|
cache = await self.get_cached_data()
|
||||||
|
data = cache.sorted_by_name
|
||||||
|
|
||||||
|
# Define letter categories
|
||||||
|
letters = {
|
||||||
|
'#': 0, # Numbers
|
||||||
|
'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'G': 0, 'H': 0,
|
||||||
|
'I': 0, 'J': 0, 'K': 0, 'L': 0, 'M': 0, 'N': 0, 'O': 0, 'P': 0,
|
||||||
|
'Q': 0, 'R': 0, 'S': 0, 'T': 0, 'U': 0, 'V': 0, 'W': 0, 'X': 0,
|
||||||
|
'Y': 0, 'Z': 0,
|
||||||
|
'@': 0, # Special characters
|
||||||
|
'漢': 0 # CJK characters
|
||||||
|
}
|
||||||
|
|
||||||
|
# Count models for each letter
|
||||||
|
for lora in data:
|
||||||
|
model_name = lora.get('model_name', '')
|
||||||
|
if not model_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_char = model_name[0].upper()
|
||||||
|
|
||||||
|
if first_char.isdigit():
|
||||||
|
letters['#'] += 1
|
||||||
|
elif first_char in letters:
|
||||||
|
letters[first_char] += 1
|
||||||
|
elif self._is_cjk_character(first_char):
|
||||||
|
letters['漢'] += 1
|
||||||
|
elif not first_char.isalnum():
|
||||||
|
letters['@'] += 1
|
||||||
|
|
||||||
|
return letters
|
||||||
|
|
||||||
async def _update_metadata_paths(self, metadata_path: str, lora_path: str) -> Dict:
|
async def _update_metadata_paths(self, metadata_path: str, lora_path: str) -> Dict:
|
||||||
"""Update file paths in metadata file"""
|
"""Update file paths in metadata file"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
89
static/css/components/alphabet-bar.css
Normal file
89
static/css/components/alphabet-bar.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/* Alphabet Bar Component */
|
||||||
|
.alphabet-bar {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: 4px;
|
||||||
|
margin: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip {
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85em;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip:hover {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip.active {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip .count {
|
||||||
|
font-size: 0.75em;
|
||||||
|
margin-left: 2px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-bar-section {
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 0 2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet-bar-title {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-right: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.alphabet-bar {
|
||||||
|
padding: 3px;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip {
|
||||||
|
padding: 3px 5px;
|
||||||
|
min-width: 16px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyframe animations for the active letter */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter-chip.active {
|
||||||
|
animation: pulse 1s ease-in-out 1;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
@import 'components/filter-indicator.css';
|
@import 'components/filter-indicator.css';
|
||||||
@import 'components/initialization.css';
|
@import 'components/initialization.css';
|
||||||
@import 'components/progress-panel.css';
|
@import 'components/progress-panel.css';
|
||||||
|
@import 'components/alphabet-bar.css'; /* Add alphabet bar component */
|
||||||
|
|
||||||
.initialization-notice {
|
.initialization-notice {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export async function loadMoreModels(options = {}) {
|
|||||||
if (pageState.showFavoritesOnly) {
|
if (pageState.showFavoritesOnly) {
|
||||||
params.append('favorites_only', 'true');
|
params.append('favorites_only', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add active letter filter if set
|
||||||
|
if (pageState.activeLetterFilter) {
|
||||||
|
params.append('first_letter', pageState.activeLetterFilter);
|
||||||
|
}
|
||||||
|
|
||||||
// Add search parameters if there's a search term
|
// Add search parameters if there's a search term
|
||||||
if (pageState.filters?.search) {
|
if (pageState.filters?.search) {
|
||||||
|
|||||||
222
static/js/components/alphabet/AlphabetBar.js
Normal file
222
static/js/components/alphabet/AlphabetBar.js
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
// AlphabetBar.js - Component for alphabet filtering
|
||||||
|
import { getCurrentPageState, setCurrentPageType } from '../../state/index.js';
|
||||||
|
import { getStorageItem, setStorageItem } from '../../utils/storageHelpers.js';
|
||||||
|
import { resetAndReload } from '../../api/loraApi.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AlphabetBar class - Handles the alphabet filtering UI and interactions
|
||||||
|
*/
|
||||||
|
export class AlphabetBar {
|
||||||
|
constructor(pageType = 'loras') {
|
||||||
|
// Store the page type
|
||||||
|
this.pageType = pageType;
|
||||||
|
|
||||||
|
// Get the current page state
|
||||||
|
this.pageState = getCurrentPageState();
|
||||||
|
|
||||||
|
// Initialize letter counts
|
||||||
|
this.letterCounts = {};
|
||||||
|
|
||||||
|
// Initialize the component
|
||||||
|
this.initializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the alphabet bar component
|
||||||
|
*/
|
||||||
|
async initializeComponent() {
|
||||||
|
// Get letter counts from API
|
||||||
|
await this.fetchLetterCounts();
|
||||||
|
|
||||||
|
// Initialize event listeners
|
||||||
|
this.initEventListeners();
|
||||||
|
|
||||||
|
// Restore the active letter filter from storage if available
|
||||||
|
this.restoreActiveLetterFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch letter counts from the API
|
||||||
|
*/
|
||||||
|
async fetchLetterCounts() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/loras/letter-counts');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch letter counts: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.letter_counts) {
|
||||||
|
this.letterCounts = data.letter_counts;
|
||||||
|
|
||||||
|
// Update the count display in the UI
|
||||||
|
this.updateLetterCountsDisplay();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching letter counts:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the letter counts display in the UI
|
||||||
|
*/
|
||||||
|
updateLetterCountsDisplay() {
|
||||||
|
const letterChips = document.querySelectorAll('.letter-chip');
|
||||||
|
|
||||||
|
letterChips.forEach(chip => {
|
||||||
|
const letter = chip.dataset.letter;
|
||||||
|
const countSpan = chip.querySelector('.count');
|
||||||
|
const count = this.letterCounts[letter] || 0;
|
||||||
|
|
||||||
|
// Update the count display
|
||||||
|
if (countSpan) {
|
||||||
|
if (count > 0) {
|
||||||
|
countSpan.textContent = ` (${count})`;
|
||||||
|
chip.title = `${letter}: ${count} LoRAs`;
|
||||||
|
chip.classList.remove('disabled');
|
||||||
|
} else {
|
||||||
|
countSpan.textContent = '';
|
||||||
|
chip.title = `${letter}: No LoRAs`;
|
||||||
|
chip.classList.add('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize event listeners for the alphabet bar
|
||||||
|
*/
|
||||||
|
initEventListeners() {
|
||||||
|
const alphabetBar = document.querySelector('.alphabet-bar');
|
||||||
|
|
||||||
|
if (alphabetBar) {
|
||||||
|
// Use event delegation for letter chips
|
||||||
|
alphabetBar.addEventListener('click', (e) => {
|
||||||
|
const letterChip = e.target.closest('.letter-chip');
|
||||||
|
|
||||||
|
if (letterChip && !letterChip.classList.contains('disabled')) {
|
||||||
|
this.handleLetterClick(letterChip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add keyboard shortcut listeners
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Alt + letter shortcuts
|
||||||
|
if (e.altKey && !e.ctrlKey && !e.metaKey) {
|
||||||
|
const key = e.key.toUpperCase();
|
||||||
|
|
||||||
|
// Check if it's a letter A-Z
|
||||||
|
if (/^[A-Z]$/.test(key)) {
|
||||||
|
const letterChip = document.querySelector(`.letter-chip[data-letter="${key}"]`);
|
||||||
|
|
||||||
|
if (letterChip && !letterChip.classList.contains('disabled')) {
|
||||||
|
this.handleLetterClick(letterChip);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Special cases for non-letter filters
|
||||||
|
else if (e.key === '0' || e.key === ')') {
|
||||||
|
// Alt+0 for numbers (#)
|
||||||
|
const letterChip = document.querySelector('.letter-chip[data-letter="#"]');
|
||||||
|
|
||||||
|
if (letterChip && !letterChip.classList.contains('disabled')) {
|
||||||
|
this.handleLetterClick(letterChip);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (e.key === '2' || e.key === '@') {
|
||||||
|
// Alt+@ for special characters
|
||||||
|
const letterChip = document.querySelector('.letter-chip[data-letter="@"]');
|
||||||
|
|
||||||
|
if (letterChip && !letterChip.classList.contains('disabled')) {
|
||||||
|
this.handleLetterClick(letterChip);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'c' || e.key === 'C') {
|
||||||
|
// Alt+C for CJK characters
|
||||||
|
const letterChip = document.querySelector('.letter-chip[data-letter="漢"]');
|
||||||
|
|
||||||
|
if (letterChip && !letterChip.classList.contains('disabled')) {
|
||||||
|
this.handleLetterClick(letterChip);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle letter chip click
|
||||||
|
* @param {HTMLElement} letterChip - The letter chip that was clicked
|
||||||
|
*/
|
||||||
|
handleLetterClick(letterChip) {
|
||||||
|
const letter = letterChip.dataset.letter;
|
||||||
|
const wasActive = letterChip.classList.contains('active');
|
||||||
|
|
||||||
|
// Remove active class from all letter chips
|
||||||
|
document.querySelectorAll('.letter-chip').forEach(chip => {
|
||||||
|
chip.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wasActive) {
|
||||||
|
// Set the new active letter
|
||||||
|
letterChip.classList.add('active');
|
||||||
|
this.pageState.activeLetterFilter = letter;
|
||||||
|
|
||||||
|
// Save to storage
|
||||||
|
setStorageItem(`${this.pageType}_activeLetterFilter`, letter);
|
||||||
|
} else {
|
||||||
|
// Clear the active letter filter
|
||||||
|
this.pageState.activeLetterFilter = null;
|
||||||
|
|
||||||
|
// Remove from storage
|
||||||
|
setStorageItem(`${this.pageType}_activeLetterFilter`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a reload with the new filter
|
||||||
|
resetAndReload(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the active letter filter from storage
|
||||||
|
*/
|
||||||
|
restoreActiveLetterFilter() {
|
||||||
|
const activeLetterFilter = getStorageItem(`${this.pageType}_activeLetterFilter`);
|
||||||
|
|
||||||
|
if (activeLetterFilter) {
|
||||||
|
const letterChip = document.querySelector(`.letter-chip[data-letter="${activeLetterFilter}"]`);
|
||||||
|
|
||||||
|
if (letterChip && !letterChip.classList.contains('disabled')) {
|
||||||
|
letterChip.classList.add('active');
|
||||||
|
this.pageState.activeLetterFilter = activeLetterFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the active letter filter
|
||||||
|
*/
|
||||||
|
clearActiveLetterFilter() {
|
||||||
|
// Remove active class from all letter chips
|
||||||
|
document.querySelectorAll('.letter-chip').forEach(chip => {
|
||||||
|
chip.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the active letter filter
|
||||||
|
this.pageState.activeLetterFilter = null;
|
||||||
|
|
||||||
|
// Remove from storage
|
||||||
|
setStorageItem(`${this.pageType}_activeLetterFilter`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update letter counts with new data
|
||||||
|
* @param {Object} newCounts - New letter count data
|
||||||
|
*/
|
||||||
|
updateCounts(newCounts) {
|
||||||
|
this.letterCounts = { ...newCounts };
|
||||||
|
this.updateLetterCountsDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
static/js/components/alphabet/index.js
Normal file
14
static/js/components/alphabet/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Alphabet component index file
|
||||||
|
import { AlphabetBar } from './AlphabetBar.js';
|
||||||
|
|
||||||
|
// Export the class
|
||||||
|
export { AlphabetBar };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create the appropriate alphabet bar
|
||||||
|
* @param {string} pageType - The type of page ('loras' or 'checkpoints')
|
||||||
|
* @returns {AlphabetBar} - The alphabet bar instance
|
||||||
|
*/
|
||||||
|
export function createAlphabetBar(pageType) {
|
||||||
|
return new AlphabetBar(pageType);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { PageControls } from './PageControls.js';
|
import { PageControls } from './PageControls.js';
|
||||||
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
|
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
|
||||||
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||||
|
import { createAlphabetBar } from '../alphabet/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LorasControls class - Extends PageControls for LoRA-specific functionality
|
* LorasControls class - Extends PageControls for LoRA-specific functionality
|
||||||
@@ -16,6 +17,9 @@ export class LorasControls extends PageControls {
|
|||||||
|
|
||||||
// Check for custom filters (e.g., from recipe navigation)
|
// Check for custom filters (e.g., from recipe navigation)
|
||||||
this.checkCustomFilters();
|
this.checkCustomFilters();
|
||||||
|
|
||||||
|
// Initialize alphabet bar component
|
||||||
|
this.initAlphabetBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,4 +146,27 @@ export class LorasControls extends PageControls {
|
|||||||
_truncateText(text, maxLength) {
|
_truncateText(text, maxLength) {
|
||||||
return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
|
return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the alphabet bar component
|
||||||
|
*/
|
||||||
|
initAlphabetBar() {
|
||||||
|
// Create the alphabet bar component
|
||||||
|
this.alphabetBar = createAlphabetBar('loras');
|
||||||
|
|
||||||
|
// Expose the alphabet bar to the global scope for debugging
|
||||||
|
window.alphabetBar = this.alphabetBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override resetAndReload to update letter counts
|
||||||
|
*/
|
||||||
|
async resetAndReload(updateFolders = false) {
|
||||||
|
await super.resetAndReload(updateFolders);
|
||||||
|
|
||||||
|
// Update letter counts after reload if alphabet bar exists
|
||||||
|
if (this.alphabetBar) {
|
||||||
|
this.alphabetBar.fetchLetterCounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ export const state = {
|
|||||||
hasMore: true,
|
hasMore: true,
|
||||||
sortBy: 'name',
|
sortBy: 'name',
|
||||||
activeFolder: null,
|
activeFolder: null,
|
||||||
|
activeLetterFilter: null, // New property for letter filtering
|
||||||
previewVersions: loraPreviewVersions,
|
previewVersions: loraPreviewVersions,
|
||||||
searchManager: null,
|
searchManager: null,
|
||||||
searchOptions: {
|
searchOptions: {
|
||||||
|
|||||||
19
templates/components/alphabet_bar.html
Normal file
19
templates/components/alphabet_bar.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="alphabet-bar-section">
|
||||||
|
<div class="alphabet-bar">
|
||||||
|
<span class="alphabet-bar-title">Filter by:</span>
|
||||||
|
<div class="letter-chip" data-letter="#" title="Numbers">
|
||||||
|
#<span class="count"></span>
|
||||||
|
</div>
|
||||||
|
{% for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %}
|
||||||
|
<div class="letter-chip" data-letter="{{ letter }}" title="{{ letter }}">
|
||||||
|
{{ letter }}<span class="count"></span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="letter-chip" data-letter="@" title="Special characters">
|
||||||
|
@<span class="count"></span>
|
||||||
|
</div>
|
||||||
|
<div class="letter-chip" data-letter="漢" title="CJK characters">
|
||||||
|
漢<span class="count"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'components/controls.html' %}
|
{% include 'components/controls.html' %}
|
||||||
|
{% include 'components/alphabet_bar.html' %}
|
||||||
<!-- Lora卡片容器 -->
|
<!-- Lora卡片容器 -->
|
||||||
<div class="card-grid" id="loraGrid">
|
<div class="card-grid" id="loraGrid">
|
||||||
<!-- Cards will be dynamically inserted here -->
|
<!-- Cards will be dynamically inserted here -->
|
||||||
|
|||||||
Reference in New Issue
Block a user