feat: implement alphabet filtering feature with letter counts and UI components v1

This commit is contained in:
Will Miao
2025-05-01 20:07:12 +08:00
parent 9dbcc105e7
commit d1fd5b7f27
11 changed files with 509 additions and 2 deletions

View 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;
}

View File

@@ -21,6 +21,7 @@
@import 'components/filter-indicator.css';
@import 'components/initialization.css';
@import 'components/progress-panel.css';
@import 'components/alphabet-bar.css'; /* Add alphabet bar component */
.initialization-notice {
display: flex;

View File

@@ -49,6 +49,11 @@ export async function loadMoreModels(options = {}) {
if (pageState.showFavoritesOnly) {
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
if (pageState.filters?.search) {

View 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();
}
}

View 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);
}

View File

@@ -2,6 +2,7 @@
import { PageControls } from './PageControls.js';
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
import { createAlphabetBar } from '../alphabet/index.js';
/**
* 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)
this.checkCustomFilters();
// Initialize alphabet bar component
this.initAlphabetBar();
}
/**
@@ -142,4 +146,27 @@ export class LorasControls extends PageControls {
_truncateText(text, maxLength) {
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();
}
}
}

View File

@@ -27,6 +27,7 @@ export const state = {
hasMore: true,
sortBy: 'name',
activeFolder: null,
activeLetterFilter: null, // New property for letter filtering
previewVersions: loraPreviewVersions,
searchManager: null,
searchOptions: {