diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py index 7ecc994b..72242daa 100644 --- a/py/services/base_model_service.py +++ b/py/services/base_model_service.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Type, Set +from typing import Dict, List, Optional, Type import logging from ..utils.models import BaseModelMetadata @@ -34,7 +34,7 @@ class BaseModelService(ABC): Args: page: Page number (1-based) 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 search: Search term fuzzy_search: Whether to use fuzzy search @@ -50,6 +50,17 @@ class BaseModelService(ABC): """ 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 if search_options is None: search_options = { @@ -59,8 +70,8 @@ class BaseModelService(ABC): 'recursive': False, } - # Get the base data set - filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name + # Get the base data set using new sort logic + filtered_data = await cache.get_sorted_data(sort_key, order) # Apply hash filtering if provided (highest priority) if hash_filters: diff --git a/py/services/lora_service.py b/py/services/lora_service.py index 1623f571..7649f75b 100644 --- a/py/services/lora_service.py +++ b/py/services/lora_service.py @@ -115,7 +115,7 @@ class LoraService(BaseModelService): async def get_letter_counts(self) -> Dict[str, int]: """Get count of LoRAs for each letter of the alphabet""" cache = await self.scanner.get_cached_data() - data = cache.sorted_by_name + data = cache.raw_data # Define letter categories letters = { diff --git a/py/services/model_cache.py b/py/services/model_cache.py index 8494531e..f67b2444 100644 --- a/py/services/model_cache.py +++ b/py/services/model_cache.py @@ -1,37 +1,85 @@ import asyncio -from typing import List, Dict +from typing import List, Dict, Tuple from dataclasses import dataclass from operator import itemgetter 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 class ModelCache: - """Cache structure for model data""" + """Cache structure for model data with extensible sorting""" raw_data: List[Dict] - sorted_by_name: List[Dict] - sorted_by_date: List[Dict] folders: List[str] def __post_init__(self): 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): - """Resort all cached data views""" + async def resort(self): + """Resort cached data according to last sort mode if set""" async with self._lock: - self.sorted_by_name = natsorted( - self.raw_data, - key=lambda x: x['model_name'].lower() # Case-insensitive sort - ) - if not name_only: - self.sorted_by_date = sorted( - self.raw_data, - key=itemgetter('modified'), - reverse=True - ) - # Update folder list + if self._last_sort != (None, None): + sort_key, order = self._last_sort + sorted_data = self._sort_data(self.raw_data, sort_key, order) + self._last_sorted_data = sorted_data + # Update folder list + # else: do nothing + all_folders = set(l['folder'] for l in self.raw_data) 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: """Update preview_url for a specific model in all cached data diff --git a/py/services/model_scanner.py b/py/services/model_scanner.py index a31bff42..0d41fe11 100644 --- a/py/services/model_scanner.py +++ b/py/services/model_scanner.py @@ -246,8 +246,6 @@ class ModelScanner: # Load data into memory self._cache = ModelCache( raw_data=cache_data["raw_data"], - sorted_by_name=[], - sorted_by_date=[], folders=[] ) @@ -280,8 +278,6 @@ class ModelScanner: if self._cache is None: self._cache = ModelCache( raw_data=[], - sorted_by_name=[], - sorted_by_date=[], folders=[] ) @@ -544,8 +540,6 @@ class ModelScanner: if self._cache is None and not force_refresh: return ModelCache( raw_data=[], - sorted_by_name=[], - sorted_by_date=[], folders=[] ) @@ -605,8 +599,6 @@ class ModelScanner: # Update cache self._cache = ModelCache( raw_data=raw_data, - sorted_by_name=[], - sorted_by_date=[], folders=[] ) @@ -620,8 +612,6 @@ class ModelScanner: if self._cache is None: self._cache = ModelCache( raw_data=[], - sorted_by_name=[], - sorted_by_date=[], folders=[] ) finally: diff --git a/static/css/layout.css b/static/css/layout.css index b948a5f2..182e50b5 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -182,6 +182,31 @@ 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 { border-color: var(--lora-accent); background-color: var(--bg-color); diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js index 42ad0062..53a2d8ef 100644 --- a/static/js/components/controls/PageControls.js +++ b/static/js/components/controls/PageControls.js @@ -1,5 +1,5 @@ // 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 { showToast } from '../../utils/uiHelpers.js'; @@ -41,6 +41,9 @@ export class PageControls { this.pageState.isLoading = false; this.pageState.hasMore = true; + // Set default sort based on page type + this.pageState.sortBy = this.pageType === 'loras' ? 'name:asc' : 'name:asc'; + // Load sort preference this.loadSortPreference(); } @@ -326,14 +329,36 @@ export class PageControls { loadSortPreference() { const savedSort = getStorageItem(`${this.pageType}_sort`); if (savedSort) { - this.pageState.sortBy = savedSort; + // Handle legacy format conversion + const convertedSort = this.convertLegacySortFormat(savedSort); + this.pageState.sortBy = convertedSort; const sortSelect = document.getElementById('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 * @param {string} sortValue - The sort value to save diff --git a/templates/components/controls.html b/templates/components/controls.html index 65b53350..3c36b1bd 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -11,8 +11,18 @@