refactor: Enhance sorting functionality and UI for model selection, including legacy format conversion

This commit is contained in:
Will Miao
2025-07-24 09:26:15 +08:00
parent cf9fd2d5c2
commit e8ccdabe6c
7 changed files with 146 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Type, Set from typing import Dict, List, Optional, Type
import logging import logging
from ..utils.models import BaseModelMetadata from ..utils.models import BaseModelMetadata
@@ -34,7 +34,7 @@ class BaseModelService(ABC):
Args: Args:
page: Page number (1-based) page: Page number (1-based)
page_size: Number of items per page page_size: Number of items per page
sort_by: Sort criteria ('name' or 'date') sort_by: Sort criteria, e.g. 'name', 'name:asc', 'name:desc', 'date', 'date:asc', 'date:desc'
folder: Folder filter folder: Folder filter
search: Search term search: Search term
fuzzy_search: Whether to use fuzzy search fuzzy_search: Whether to use fuzzy search
@@ -50,6 +50,17 @@ class BaseModelService(ABC):
""" """
cache = await self.scanner.get_cached_data() cache = await self.scanner.get_cached_data()
# Parse sort_by into sort_key and order
if ':' in sort_by:
sort_key, order = sort_by.split(':', 1)
sort_key = sort_key.strip()
order = order.strip().lower()
if order not in ('asc', 'desc'):
order = 'asc'
else:
sort_key = sort_by.strip()
order = 'asc'
# Get default search options if not provided # Get default search options if not provided
if search_options is None: if search_options is None:
search_options = { search_options = {
@@ -59,8 +70,8 @@ class BaseModelService(ABC):
'recursive': False, 'recursive': False,
} }
# Get the base data set # Get the base data set using new sort logic
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name filtered_data = await cache.get_sorted_data(sort_key, order)
# Apply hash filtering if provided (highest priority) # Apply hash filtering if provided (highest priority)
if hash_filters: if hash_filters:

View File

@@ -115,7 +115,7 @@ class LoraService(BaseModelService):
async def get_letter_counts(self) -> Dict[str, int]: async def get_letter_counts(self) -> Dict[str, int]:
"""Get count of LoRAs for each letter of the alphabet""" """Get count of LoRAs for each letter of the alphabet"""
cache = await self.scanner.get_cached_data() cache = await self.scanner.get_cached_data()
data = cache.sorted_by_name data = cache.raw_data
# Define letter categories # Define letter categories
letters = { letters = {

View File

@@ -1,37 +1,85 @@
import asyncio import asyncio
from typing import List, Dict from typing import List, Dict, Tuple
from dataclasses import dataclass from dataclasses import dataclass
from operator import itemgetter from operator import itemgetter
from natsort import natsorted from natsort import natsorted
# Supported sort modes: (sort_key, order)
# order: 'asc' for ascending, 'desc' for descending
SUPPORTED_SORT_MODES = [
('name', 'asc'),
('name', 'desc'),
('date', 'asc'),
('date', 'desc'),
('size', 'asc'),
('size', 'desc'),
]
@dataclass @dataclass
class ModelCache: class ModelCache:
"""Cache structure for model data""" """Cache structure for model data with extensible sorting"""
raw_data: List[Dict] raw_data: List[Dict]
sorted_by_name: List[Dict]
sorted_by_date: List[Dict]
folders: List[str] folders: List[str]
def __post_init__(self): def __post_init__(self):
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
# Cache for last sort: (sort_key, order) -> sorted list
self._last_sort: Tuple[str, str] = (None, None)
self._last_sorted_data: List[Dict] = []
# Default sort on init
asyncio.create_task(self.resort())
async def resort(self, name_only: bool = False): async def resort(self):
"""Resort all cached data views""" """Resort cached data according to last sort mode if set"""
async with self._lock: async with self._lock:
self.sorted_by_name = natsorted( if self._last_sort != (None, None):
self.raw_data, sort_key, order = self._last_sort
key=lambda x: x['model_name'].lower() # Case-insensitive sort sorted_data = self._sort_data(self.raw_data, sort_key, order)
) self._last_sorted_data = sorted_data
if not name_only: # Update folder list
self.sorted_by_date = sorted( # else: do nothing
self.raw_data,
key=itemgetter('modified'),
reverse=True
)
# Update folder list
all_folders = set(l['folder'] for l in self.raw_data) all_folders = set(l['folder'] for l in self.raw_data)
self.folders = sorted(list(all_folders), key=lambda x: x.lower()) self.folders = sorted(list(all_folders), key=lambda x: x.lower())
def _sort_data(self, data: List[Dict], sort_key: str, order: str) -> List[Dict]:
"""Sort data by sort_key and order"""
reverse = (order == 'desc')
if sort_key == 'name':
# Natural sort by model_name, case-insensitive
return natsorted(
data,
key=lambda x: x['model_name'].lower(),
reverse=reverse
)
elif sort_key == 'date':
# Sort by modified timestamp
return sorted(
data,
key=itemgetter('modified'),
reverse=reverse
)
elif sort_key == 'size':
# Sort by file size
return sorted(
data,
key=itemgetter('size'),
reverse=reverse
)
else:
# Fallback: no sort
return list(data)
async def get_sorted_data(self, sort_key: str = 'name', order: str = 'asc') -> List[Dict]:
"""Get sorted data by sort_key and order, using cache if possible"""
async with self._lock:
if (sort_key, order) == self._last_sort:
return self._last_sorted_data
sorted_data = self._sort_data(self.raw_data, sort_key, order)
self._last_sort = (sort_key, order)
self._last_sorted_data = sorted_data
return sorted_data
async def update_preview_url(self, file_path: str, preview_url: str, preview_nsfw_level: int) -> bool: async def update_preview_url(self, file_path: str, preview_url: str, preview_nsfw_level: int) -> bool:
"""Update preview_url for a specific model in all cached data """Update preview_url for a specific model in all cached data

View File

@@ -246,8 +246,6 @@ class ModelScanner:
# Load data into memory # Load data into memory
self._cache = ModelCache( self._cache = ModelCache(
raw_data=cache_data["raw_data"], raw_data=cache_data["raw_data"],
sorted_by_name=[],
sorted_by_date=[],
folders=[] folders=[]
) )
@@ -280,8 +278,6 @@ class ModelScanner:
if self._cache is None: if self._cache is None:
self._cache = ModelCache( self._cache = ModelCache(
raw_data=[], raw_data=[],
sorted_by_name=[],
sorted_by_date=[],
folders=[] folders=[]
) )
@@ -544,8 +540,6 @@ class ModelScanner:
if self._cache is None and not force_refresh: if self._cache is None and not force_refresh:
return ModelCache( return ModelCache(
raw_data=[], raw_data=[],
sorted_by_name=[],
sorted_by_date=[],
folders=[] folders=[]
) )
@@ -605,8 +599,6 @@ class ModelScanner:
# Update cache # Update cache
self._cache = ModelCache( self._cache = ModelCache(
raw_data=raw_data, raw_data=raw_data,
sorted_by_name=[],
sorted_by_date=[],
folders=[] folders=[]
) )
@@ -620,8 +612,6 @@ class ModelScanner:
if self._cache is None: if self._cache is None:
self._cache = ModelCache( self._cache = ModelCache(
raw_data=[], raw_data=[],
sorted_by_name=[],
sorted_by_date=[],
folders=[] folders=[]
) )
finally: finally:

View File

@@ -182,6 +182,31 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
/* Style for optgroups */
.control-group select optgroup {
font-weight: 600;
font-style: normal;
color: var(--text-color);
background-color: var(--card-bg);
}
.control-group select option {
padding: 4px 8px;
background-color: var(--card-bg);
color: var(--text-color);
}
/* Dark theme optgroup styling */
[data-theme="dark"] .control-group select optgroup {
background-color: var(--card-bg);
color: var(--text-color);
}
[data-theme="dark"] .control-group select option {
background-color: var(--card-bg);
color: var(--text-color);
}
.control-group select:hover { .control-group select:hover {
border-color: var(--lora-accent); border-color: var(--lora-accent);
background-color: var(--bg-color); background-color: var(--bg-color);

View File

@@ -1,5 +1,5 @@
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages // PageControls.js - Manages controls for both LoRAs and Checkpoints pages
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js'; import { getCurrentPageState, setCurrentPageType } from '../../state/index.js';
import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js'; import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js';
import { showToast } from '../../utils/uiHelpers.js'; import { showToast } from '../../utils/uiHelpers.js';
@@ -41,6 +41,9 @@ export class PageControls {
this.pageState.isLoading = false; this.pageState.isLoading = false;
this.pageState.hasMore = true; this.pageState.hasMore = true;
// Set default sort based on page type
this.pageState.sortBy = this.pageType === 'loras' ? 'name:asc' : 'name:asc';
// Load sort preference // Load sort preference
this.loadSortPreference(); this.loadSortPreference();
} }
@@ -326,14 +329,36 @@ export class PageControls {
loadSortPreference() { loadSortPreference() {
const savedSort = getStorageItem(`${this.pageType}_sort`); const savedSort = getStorageItem(`${this.pageType}_sort`);
if (savedSort) { if (savedSort) {
this.pageState.sortBy = savedSort; // Handle legacy format conversion
const convertedSort = this.convertLegacySortFormat(savedSort);
this.pageState.sortBy = convertedSort;
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
if (sortSelect) { if (sortSelect) {
sortSelect.value = savedSort; sortSelect.value = convertedSort;
} }
} }
} }
/**
* Convert legacy sort format to new format
* @param {string} sortValue - The sort value to convert
* @returns {string} - Converted sort value
*/
convertLegacySortFormat(sortValue) {
// Convert old format to new format with direction
switch (sortValue) {
case 'name':
return 'name:asc';
case 'date':
return 'date:desc'; // Newest first is more intuitive default
case 'size':
return 'size:desc'; // Largest first is more intuitive default
default:
// If it's already in new format or unknown, return as is
return sortValue.includes(':') ? sortValue : 'name:asc';
}
}
/** /**
* Save sort preference to storage * Save sort preference to storage
* @param {string} sortValue - The sort value to save * @param {string} sortValue - The sort value to save

View File

@@ -11,8 +11,18 @@
<div class="action-buttons"> <div class="action-buttons">
<div title="Sort models by..." class="control-group"> <div title="Sort models by..." class="control-group">
<select id="sortSelect"> <select id="sortSelect">
<option value="name">Name</option> <optgroup label="Name">
<option value="date">Date</option> <option value="name:asc">A - Z</option>
<option value="name:desc">Z - A</option>
</optgroup>
<optgroup label="Date Added">
<option value="date:desc">Newest</option>
<option value="date:asc">Oldest</option>
</optgroup>
<optgroup label="File Size">
<option value="size:desc">Largest</option>
<option value="size:asc">Smallest</option>
</optgroup>
</select> </select>
</div> </div>
<div title="Refresh model list" class="control-group dropdown-group"> <div title="Refresh model list" class="control-group dropdown-group">