mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Update recipes page with default descending date sort, refactor state properties for search/filters, and add new localization strings.
This commit is contained in:
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "Bitte wählen Sie ein LoRA-Stammverzeichnis aus"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "Rezepte sortieren nach...",
|
||||
"name": "Name",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "Datum",
|
||||
"dateDesc": "Neueste",
|
||||
"dateAsc": "Älteste",
|
||||
"lorasCount": "LoRA-Anzahl",
|
||||
"lorasCountDesc": "Meiste",
|
||||
"lorasCountAsc": "Wenigste"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "Rezeptliste aktualisieren"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "Please select a LoRA root directory"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "Sort recipes by...",
|
||||
"name": "Name",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "Date",
|
||||
"dateDesc": "Newest",
|
||||
"dateAsc": "Oldest",
|
||||
"lorasCount": "LoRA Count",
|
||||
"lorasCountDesc": "Most",
|
||||
"lorasCountAsc": "Least"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "Refresh recipe list"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "Por favor selecciona un directorio raíz de LoRA"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "Ordenar recetas por...",
|
||||
"name": "Nombre",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "Fecha",
|
||||
"dateDesc": "Más reciente",
|
||||
"dateAsc": "Más antiguo",
|
||||
"lorasCount": "Cant. de LoRAs",
|
||||
"lorasCountDesc": "Más",
|
||||
"lorasCountAsc": "Menos"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "Actualizar lista de recetas"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "Veuillez sélectionner un répertoire racine LoRA"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "Trier les recettes par...",
|
||||
"name": "Nom",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "Date",
|
||||
"dateDesc": "Plus récent",
|
||||
"dateAsc": "Plus ancien",
|
||||
"lorasCount": "Nombre de LoRAs",
|
||||
"lorasCountDesc": "Plus",
|
||||
"lorasCountAsc": "Moins"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "Actualiser la liste des recipes"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "אנא בחר ספריית שורש של LoRA"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "מיון מתכונים לפי...",
|
||||
"name": "שם",
|
||||
"nameAsc": "א - ת",
|
||||
"nameDesc": "ת - א",
|
||||
"date": "תאריך",
|
||||
"dateDesc": "הכי חדש",
|
||||
"dateAsc": "הכי ישן",
|
||||
"lorasCount": "מספר LoRAs",
|
||||
"lorasCountDesc": "הכי הרבה",
|
||||
"lorasCountAsc": "הכי פחות"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "רענן רשימת מתכונים"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "LoRAルートディレクトリを選択してください"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "レシピの並び替え...",
|
||||
"name": "名前",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "日付",
|
||||
"dateDesc": "新しい順",
|
||||
"dateAsc": "古い順",
|
||||
"lorasCount": "LoRA数",
|
||||
"lorasCountDesc": "多い順",
|
||||
"lorasCountAsc": "少ない順"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "レシピリストを更新"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "LoRA 루트 디렉토리를 선택해주세요"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "레시피 정렬...",
|
||||
"name": "이름",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "날짜",
|
||||
"dateDesc": "최신순",
|
||||
"dateAsc": "오래된순",
|
||||
"lorasCount": "LoRA 수",
|
||||
"lorasCountDesc": "많은순",
|
||||
"lorasCountAsc": "적은순"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "레시피 목록 새로고침"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "Пожалуйста, выберите корневую папку LoRA"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "Сортировка рецептов...",
|
||||
"name": "Имя",
|
||||
"nameAsc": "А - Я",
|
||||
"nameDesc": "Я - А",
|
||||
"date": "Дата",
|
||||
"dateDesc": "Сначала новые",
|
||||
"dateAsc": "Сначала старые",
|
||||
"lorasCount": "Кол-во LoRA",
|
||||
"lorasCountDesc": "Больше всего",
|
||||
"lorasCountAsc": "Меньше всего"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "Обновить список рецептов"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "请选择 LoRA 根目录"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "配方排序...",
|
||||
"name": "名称",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "时间",
|
||||
"dateDesc": "最新",
|
||||
"dateAsc": "最早",
|
||||
"lorasCount": "LoRA 数量",
|
||||
"lorasCountDesc": "最多",
|
||||
"lorasCountAsc": "最少"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "刷新配方列表"
|
||||
},
|
||||
|
||||
@@ -589,6 +589,18 @@
|
||||
"selectLoraRoot": "請選擇 LoRA 根目錄"
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"title": "配方排序...",
|
||||
"name": "名稱",
|
||||
"nameAsc": "A - Z",
|
||||
"nameDesc": "Z - A",
|
||||
"date": "時間",
|
||||
"dateDesc": "最新",
|
||||
"dateAsc": "最舊",
|
||||
"lorasCount": "LoRA 數量",
|
||||
"lorasCountDesc": "最多",
|
||||
"lorasCountAsc": "最少"
|
||||
},
|
||||
"refresh": {
|
||||
"title": "重新整理配方列表"
|
||||
},
|
||||
|
||||
@@ -1024,7 +1024,14 @@ class RecipeScanner:
|
||||
cache = await self.get_cached_data()
|
||||
|
||||
# Get base dataset
|
||||
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name
|
||||
sort_field = sort_by.split(':')[0] if ':' in sort_by else sort_by
|
||||
|
||||
if sort_field == 'date':
|
||||
filtered_data = list(cache.sorted_by_date)
|
||||
elif sort_field == 'name':
|
||||
filtered_data = list(cache.sorted_by_name)
|
||||
else:
|
||||
filtered_data = list(cache.raw_data)
|
||||
|
||||
# Apply SFW filtering if enabled
|
||||
from .settings_manager import get_settings_manager
|
||||
@@ -1166,6 +1173,20 @@ class RecipeScanner:
|
||||
if not any(tag in exclude_tags for tag in (item.get('tags', []) or []))
|
||||
]
|
||||
|
||||
|
||||
# Apply sorting if not already handled by pre-sorted cache
|
||||
if ':' in sort_by or sort_field == 'loras_count':
|
||||
field, order = (sort_by.split(':') + ['desc'])[:2]
|
||||
reverse = order.lower() == 'desc'
|
||||
|
||||
if field == 'name':
|
||||
filtered_data = natsorted(filtered_data, key=lambda x: x.get('title', '').lower(), reverse=reverse)
|
||||
elif field == 'date':
|
||||
# Use modified if available, falling back to created_date
|
||||
filtered_data.sort(key=lambda x: (x.get('modified', x.get('created_date', 0)), x.get('file_path', '')), reverse=reverse)
|
||||
elif field == 'loras_count':
|
||||
filtered_data.sort(key=lambda x: len(x.get('loras', [])), reverse=reverse)
|
||||
|
||||
# Calculate pagination
|
||||
total_items = len(filtered_data)
|
||||
start_idx = (page - 1) * page_size
|
||||
|
||||
@@ -216,6 +216,7 @@ class RecipeManager {
|
||||
// Sort select
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
if (sortSelect) {
|
||||
sortSelect.value = this.pageState.sortBy || 'date:desc';
|
||||
sortSelect.addEventListener('change', () => {
|
||||
this.pageState.sortBy = sortSelect.value;
|
||||
refreshVirtualScroll();
|
||||
|
||||
@@ -95,7 +95,7 @@ export const state = {
|
||||
currentPage: 1,
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'date',
|
||||
sortBy: 'date:desc',
|
||||
activeFolder: getStorageItem('recipes_activeFolder'),
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
|
||||
@@ -46,6 +46,22 @@
|
||||
<!-- Recipe controls -->
|
||||
<div class="controls">
|
||||
<div class="action-buttons">
|
||||
<div class="control-group">
|
||||
<select id="sortSelect" title="{{ t('recipes.controls.sort.title') }}">
|
||||
<optgroup label="{{ t('recipes.controls.sort.name') }}">
|
||||
<option value="name:asc">{{ t('recipes.controls.sort.nameAsc') }}</option>
|
||||
<option value="name:desc">{{ t('recipes.controls.sort.nameDesc') }}</option>
|
||||
</optgroup>
|
||||
<optgroup label="{{ t('recipes.controls.sort.date') }}">
|
||||
<option value="date:desc">{{ t('recipes.controls.sort.dateDesc') }}</option>
|
||||
<option value="date:asc">{{ t('recipes.controls.sort.dateAsc') }}</option>
|
||||
</optgroup>
|
||||
<optgroup label="{{ t('recipes.controls.sort.lorasCount') }}">
|
||||
<option value="loras_count:desc">{{ t('recipes.controls.sort.lorasCountDesc') }}</option>
|
||||
<option value="loras_count:asc">{{ t('recipes.controls.sort.lorasCountAsc') }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div title="{{ t('recipes.controls.refresh.title') }}" class="control-group">
|
||||
<button onclick="recipeManager.refreshRecipes()"><i class="fas fa-sync"></i> {{ t('common.actions.refresh')
|
||||
}}</button>
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('RecipeManager', () => {
|
||||
};
|
||||
|
||||
pageState = {
|
||||
sortBy: 'date',
|
||||
sortBy: 'date:desc',
|
||||
searchOptions: undefined,
|
||||
customFilter: undefined,
|
||||
duplicatesMode: false,
|
||||
@@ -137,8 +137,8 @@ describe('RecipeManager', () => {
|
||||
const sortSelectElement = document.createElement('select');
|
||||
sortSelectElement.id = 'sortSelect';
|
||||
sortSelectElement.innerHTML = `
|
||||
<option value="date">Date</option>
|
||||
<option value="name">Name</option>
|
||||
<option value="date:desc">Newest</option>
|
||||
<option value="name:asc">Name A-Z</option>
|
||||
`;
|
||||
document.body.appendChild(sortSelectElement);
|
||||
|
||||
@@ -183,10 +183,10 @@ describe('RecipeManager', () => {
|
||||
expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
sortSelect.value = 'name';
|
||||
sortSelect.value = 'name:asc';
|
||||
sortSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
expect(pageState.sortBy).toBe('name');
|
||||
expect(pageState.sortBy).toBe('name:asc');
|
||||
expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(2);
|
||||
expect(initializePageFeaturesMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -601,3 +601,43 @@ async def test_get_paginated_data_filters_by_prompt(recipe_scanner):
|
||||
page=1, page_size=10, search="forest", search_options={"prompt": False}
|
||||
)
|
||||
assert len(result_disabled["items"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_sorting(recipe_scanner):
|
||||
scanner, _ = recipe_scanner
|
||||
|
||||
# Add test recipes
|
||||
# Recipe A: Name "Alpha", Date 10, LoRAs 2
|
||||
await scanner.add_recipe({
|
||||
"id": "A", "title": "Alpha", "created_date": 10.0,
|
||||
"loras": [{}, {}], "file_path": "a.png"
|
||||
})
|
||||
# Recipe B: Name "Beta", Date 20, LoRAs 1
|
||||
await scanner.add_recipe({
|
||||
"id": "B", "title": "Beta", "created_date": 20.0,
|
||||
"loras": [{}], "file_path": "b.png"
|
||||
})
|
||||
# Recipe C: Name "Gamma", Date 5, LoRAs 3
|
||||
await scanner.add_recipe({
|
||||
"id": "C", "title": "Gamma", "created_date": 5.0,
|
||||
"loras": [{}, {}, {}], "file_path": "c.png"
|
||||
})
|
||||
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# Test Name DESC: Gamma, Beta, Alpha
|
||||
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="name:desc")
|
||||
assert [i["id"] for i in res["items"]] == ["C", "B", "A"]
|
||||
|
||||
# Test LoRA Count DESC: Gamma (3), Alpha (2), Beta (1)
|
||||
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="loras_count:desc")
|
||||
assert [i["id"] for i in res["items"]] == ["C", "A", "B"]
|
||||
|
||||
# Test LoRA Count ASC: Beta (1), Alpha (2), Gamma (3)
|
||||
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="loras_count:asc")
|
||||
assert [i["id"] for i in res["items"]] == ["B", "A", "C"]
|
||||
|
||||
# Test Date ASC: Gamma (5), Alpha (10), Beta (20)
|
||||
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="date:asc")
|
||||
assert [i["id"] for i in res["items"]] == ["C", "A", "B"]
|
||||
|
||||
Reference in New Issue
Block a user