mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
refactor: Enhance sorting functionality and UI for model selection, including legacy format conversion
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user