mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Introduce recipe favoriting with star icon toggle and filter options.
This commit is contained in:
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "Rezeptliste aktualisieren"
|
||||
},
|
||||
"filteredByLora": "Gefiltert nach LoRA"
|
||||
"filteredByLora": "Gefiltert nach LoRA",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "{count} Duplikat-Gruppen gefunden",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "Refresh recipe list"
|
||||
},
|
||||
"filteredByLora": "Filtered by LoRA"
|
||||
"filteredByLora": "Filtered by LoRA",
|
||||
"favorites": {
|
||||
"title": "Show Favorites Only",
|
||||
"action": "Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "Found {count} duplicate groups",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "Actualizar lista de recetas"
|
||||
},
|
||||
"filteredByLora": "Filtrado por LoRA"
|
||||
"filteredByLora": "Filtrado por LoRA",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "Se encontraron {count} grupos de duplicados",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "Actualiser la liste des recipes"
|
||||
},
|
||||
"filteredByLora": "Filtré par LoRA"
|
||||
"filteredByLora": "Filtré par LoRA",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "Trouvé {count} groupes de doublons",
|
||||
|
||||
@@ -228,6 +228,7 @@
|
||||
"videoSettings": "הגדרות וידאו",
|
||||
"layoutSettings": "הגדרות פריסה",
|
||||
"folderSettings": "הגדרות תיקייה",
|
||||
"priorityTags": "תגיות עדיפות",
|
||||
"downloadPathTemplates": "תבניות נתיב הורדה",
|
||||
"exampleImages": "תמונות דוגמה",
|
||||
"updateFlags": "תגי עדכון",
|
||||
@@ -235,8 +236,7 @@
|
||||
"misc": "שונות",
|
||||
"metadataArchive": "מסד נתונים של ארכיון מטא-דאטה",
|
||||
"storageLocation": "מיקום ההגדרות",
|
||||
"proxySettings": "הגדרות פרוקסי",
|
||||
"priorityTags": "תגיות עדיפות"
|
||||
"proxySettings": "הגדרות פרוקסי"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "מצב נייד",
|
||||
@@ -309,6 +309,26 @@
|
||||
"defaultEmbeddingRootHelp": "הגדר את ספריית השורש המוגדרת כברירת מחדל של embedding להורדות, ייבוא והעברות",
|
||||
"noDefault": "אין ברירת מחדל"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "תגיות עדיפות",
|
||||
"description": "התאם את סדר העדיפות של התגיות עבור כל סוג מודל (לדוגמה: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "פתח עזרה בנושא תגיות עדיפות",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "תגיות העדיפות עודכנו.",
|
||||
"saveError": "עדכון תגיות העדיפות נכשל.",
|
||||
"loadingSuggestions": "טוען הצעות...",
|
||||
"validation": {
|
||||
"missingClosingParen": "לרשומה {index} חסר סוגר סוגריים.",
|
||||
"missingCanonical": "על הרשומה {index} לכלול שם תגית קנונית.",
|
||||
"duplicateCanonical": "התגית הקנונית \"{tag}\" מופיעה יותר מפעם אחת.",
|
||||
"unknown": "תצורת תגיות העדיפות שגויה."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "תבניות נתיב הורדה",
|
||||
"help": "הגדר מבני תיקיות לסוגי מודלים שונים בעת הורדה מ-Civitai.",
|
||||
@@ -320,8 +340,8 @@
|
||||
"byFirstTag": "לפי תגית ראשונה",
|
||||
"baseModelFirstTag": "מודל בסיס + תגית ראשונה",
|
||||
"baseModelAuthor": "מודל בסיס + יוצר",
|
||||
"baseModelAuthorFirstTag": "מודל בסיס + יוצר + תגית ראשונה",
|
||||
"authorFirstTag": "יוצר + תגית ראשונה",
|
||||
"baseModelAuthorFirstTag": "מודל בסיס + יוצר + תגית ראשונה",
|
||||
"customTemplate": "תבנית מותאמת אישית"
|
||||
},
|
||||
"customTemplatePlaceholder": "הזן תבנית מותאמת אישית (למשל, {base_model}/{author}/{first_tag})",
|
||||
@@ -409,26 +429,6 @@
|
||||
"proxyPassword": "סיסמה (אופציונלי)",
|
||||
"proxyPasswordPlaceholder": "password",
|
||||
"proxyPasswordHelp": "סיסמה לאימות מול הפרוקסי (אם נדרש)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "תגיות עדיפות",
|
||||
"description": "התאם את סדר העדיפות של התגיות עבור כל סוג מודל (לדוגמה: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "פתח עזרה בנושא תגיות עדיפות",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "תגיות העדיפות עודכנו.",
|
||||
"saveError": "עדכון תגיות העדיפות נכשל.",
|
||||
"loadingSuggestions": "טוען הצעות...",
|
||||
"validation": {
|
||||
"missingClosingParen": "לרשומה {index} חסר סוגר סוגריים.",
|
||||
"missingCanonical": "על הרשומה {index} לכלול שם תגית קנונית.",
|
||||
"duplicateCanonical": "התגית הקנונית \"{tag}\" מופיעה יותר מפעם אחת.",
|
||||
"unknown": "תצורת תגיות העדיפות שגויה."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "רענן רשימת מתכונים"
|
||||
},
|
||||
"filteredByLora": "מסונן לפי LoRA"
|
||||
"filteredByLora": "מסונן לפי LoRA",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "נמצאו {count} קבוצות כפולות",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "レシピリストを更新"
|
||||
},
|
||||
"filteredByLora": "LoRAでフィルタ済み"
|
||||
"filteredByLora": "LoRAでフィルタ済み",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "{count} 個の重複グループが見つかりました",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "레시피 목록 새로고침"
|
||||
},
|
||||
"filteredByLora": "LoRA로 필터링됨"
|
||||
"filteredByLora": "LoRA로 필터링됨",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "{count}개의 중복 그룹 발견",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "Обновить список рецептов"
|
||||
},
|
||||
"filteredByLora": "Фильтр по LoRA"
|
||||
"filteredByLora": "Фильтр по LoRA",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "Найдено {count} групп дубликатов",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "刷新配方列表"
|
||||
},
|
||||
"filteredByLora": "按 LoRA 筛选"
|
||||
"filteredByLora": "按 LoRA 筛选",
|
||||
"favorites": {
|
||||
"title": "仅显示收藏",
|
||||
"action": "收藏"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "发现 {count} 个重复组",
|
||||
|
||||
@@ -591,7 +591,11 @@
|
||||
"refresh": {
|
||||
"title": "重新整理配方列表"
|
||||
},
|
||||
"filteredByLora": "已依 LoRA 篩選"
|
||||
"filteredByLora": "已依 LoRA 篩選",
|
||||
"favorites": {
|
||||
"title": "[TODO: Translate] Show Favorites Only",
|
||||
"action": "[TODO: Translate] Favorites"
|
||||
}
|
||||
},
|
||||
"duplicates": {
|
||||
"found": "發現 {count} 組重複項",
|
||||
|
||||
@@ -170,6 +170,9 @@ class RecipeListingHandler:
|
||||
if base_models:
|
||||
filters["base_model"] = base_models.split(",")
|
||||
|
||||
if request.query.get("favorite", "false").lower() == "true":
|
||||
filters["favorite"] = True
|
||||
|
||||
tag_filters: Dict[str, str] = {}
|
||||
legacy_tags = request.query.get("tags")
|
||||
if legacy_tags:
|
||||
|
||||
@@ -1122,6 +1122,13 @@ class RecipeScanner:
|
||||
if item.get('base_model', '') in filters['base_model']
|
||||
]
|
||||
|
||||
# Filter by favorite
|
||||
if 'favorite' in filters and filters['favorite']:
|
||||
filtered_data = [
|
||||
item for item in filtered_data
|
||||
if item.get('favorite') is True
|
||||
]
|
||||
|
||||
# Filter by tags
|
||||
if 'tags' in filters and filters['tags']:
|
||||
tag_spec = filters['tags']
|
||||
|
||||
@@ -173,9 +173,9 @@ class RecipePersistenceService:
|
||||
async def update_recipe(self, *, recipe_scanner, recipe_id: str, updates: dict[str, Any]) -> PersistenceResult:
|
||||
"""Update persisted metadata for a recipe."""
|
||||
|
||||
if not any(key in updates for key in ("title", "tags", "source_path", "preview_nsfw_level")):
|
||||
if not any(key in updates for key in ("title", "tags", "source_path", "preview_nsfw_level", "favorite")):
|
||||
raise RecipeValidationError(
|
||||
"At least one field to update must be provided (title or tags or source_path or preview_nsfw_level)"
|
||||
"At least one field to update must be provided (title or tags or source_path or preview_nsfw_level or favorite)"
|
||||
)
|
||||
|
||||
success = await recipe_scanner.update_recipe_metadata(recipe_id, updates)
|
||||
|
||||
@@ -34,7 +34,7 @@ class TranslationKeySynchronizer:
|
||||
self.locales_dir = locales_dir
|
||||
self.verbose = verbose
|
||||
self.reference_locale = 'en'
|
||||
self.target_locales = ['zh-CN', 'zh-TW', 'ja', 'ru', 'de', 'fr', 'es', 'ko']
|
||||
self.target_locales = ['zh-CN', 'zh-TW', 'ja', 'ru', 'de', 'fr', 'es', 'ko', 'he']
|
||||
|
||||
def log(self, message: str, level: str = 'INFO'):
|
||||
"""Log a message if verbose mode is enabled."""
|
||||
|
||||
@@ -47,6 +47,10 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
sort_by: pageState.sortBy
|
||||
});
|
||||
|
||||
if (pageState.showFavoritesOnly) {
|
||||
params.append('favorite', 'true');
|
||||
}
|
||||
|
||||
if (pageState.activeFolder) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
params.append('recursive', pageState.searchOptions?.recursive !== false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Recipe Card Component
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { updateRecipeMetadata } from '../api/recipeApi.js';
|
||||
import { configureModelCardVideo } from './shared/ModelCard.js';
|
||||
import { modalManager } from '../managers/ModalManager.js';
|
||||
import { getCurrentPageState } from '../state/index.js';
|
||||
@@ -44,8 +45,11 @@ class RecipeCard {
|
||||
(this.recipe.file_path ? `/loras_static/root1/preview/${this.recipe.file_path.split('/').pop()}` :
|
||||
'/loras_static/images/no-preview.png');
|
||||
|
||||
const isDuplicatesMode = getCurrentPageState().duplicatesMode;
|
||||
const autoplayOnHover = state?.global?.settings?.autoplay_on_hover === true;
|
||||
const isFavorite = this.recipe.favorite === true;
|
||||
|
||||
// Video preview logic
|
||||
const autoplayOnHover = state.settings.autoplay_on_hover || false;
|
||||
const isVideo = previewUrl.endsWith('.mp4') || previewUrl.endsWith('.webm');
|
||||
const videoAttrs = [
|
||||
'controls',
|
||||
@@ -60,10 +64,6 @@ class RecipeCard {
|
||||
videoAttrs.push('data-autoplay="true"');
|
||||
}
|
||||
|
||||
// Check if in duplicates mode
|
||||
const pageState = getCurrentPageState();
|
||||
const isDuplicatesMode = pageState.duplicatesMode;
|
||||
|
||||
// NSFW blur logic - similar to LoraCard
|
||||
const nsfwLevel = this.recipe.preview_nsfw_level !== undefined ? this.recipe.preview_nsfw_level : 0;
|
||||
const shouldBlur = state.settings.blur_mature_content && nsfwLevel > NSFW_LEVELS.PG13;
|
||||
@@ -96,6 +96,7 @@ class RecipeCard {
|
||||
</button>` : ''}
|
||||
<span class="base-model-label ${shouldBlur ? 'with-toggle' : ''}" title="${baseModelLabel}">${baseModelDisplay}</span>
|
||||
<div class="card-actions">
|
||||
<i class="${isFavorite ? 'fas fa-star favorite-active' : 'far fa-star'}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}"></i>
|
||||
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
||||
<i class="fas fa-paper-plane" title="Send Recipe to Workflow (Click: Append, Shift+Click: Replace)"></i>
|
||||
<i class="fas fa-trash" title="Delete Recipe"></i>
|
||||
@@ -141,6 +142,67 @@ class RecipeCard {
|
||||
return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||
}
|
||||
|
||||
async toggleFavorite(card) {
|
||||
// Find the latest star icon in case the card was re-rendered
|
||||
const getStarIcon = (c) => c.querySelector('.fa-star');
|
||||
let starIcon = getStarIcon(card);
|
||||
|
||||
const isFavorite = this.recipe.favorite || false;
|
||||
const newFavoriteState = !isFavorite;
|
||||
|
||||
// Update early to provide instant feedback and avoid race conditions with re-renders
|
||||
this.recipe.favorite = newFavoriteState;
|
||||
|
||||
// Function to update icon state
|
||||
const updateIconUI = (icon, state) => {
|
||||
if (!icon) return;
|
||||
if (state) {
|
||||
icon.classList.remove('far');
|
||||
icon.classList.add('fas', 'favorite-active');
|
||||
icon.title = 'Remove from Favorites';
|
||||
} else {
|
||||
icon.classList.remove('fas', 'favorite-active');
|
||||
icon.classList.add('far');
|
||||
icon.title = 'Add to Favorites';
|
||||
}
|
||||
};
|
||||
|
||||
// Update current icon immediately
|
||||
updateIconUI(starIcon, newFavoriteState);
|
||||
|
||||
try {
|
||||
await updateRecipeMetadata(this.recipe.file_path, {
|
||||
favorite: newFavoriteState
|
||||
});
|
||||
|
||||
// Status already updated, just show toast
|
||||
if (newFavoriteState) {
|
||||
showToast('modelCard.favorites.added', {}, 'success');
|
||||
} else {
|
||||
showToast('modelCard.favorites.removed', {}, 'success');
|
||||
}
|
||||
|
||||
// Re-find star icon after API call as VirtualScroller might have replaced the element
|
||||
// During updateRecipeMetadata, VirtualScroller.updateSingleItem might have re-rendered the card
|
||||
// We need to find the NEW element in the DOM to ensure we don't have a stale reference
|
||||
// Though typically VirtualScroller handles the re-render with the NEW this.recipe.favorite
|
||||
// we will check the DOM just to be sure if this instance's internal card is still what's in DOM
|
||||
} catch (error) {
|
||||
console.error('Failed to update favorite status:', error);
|
||||
// Revert local state on error
|
||||
this.recipe.favorite = isFavorite;
|
||||
|
||||
// Re-find star icon in case of re-render during fault
|
||||
const currentCard = card.ownerDocument.evaluate(
|
||||
`.//*[@data-filepath="${this.recipe.file_path}"]`,
|
||||
card.ownerDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
|
||||
).singleNodeValue || card;
|
||||
|
||||
updateIconUI(getStarIcon(currentCard), isFavorite);
|
||||
showToast('modelCard.favorites.updateFailed', {}, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
attachEventListeners(card, isDuplicatesMode, shouldBlur) {
|
||||
// Add blur toggle functionality if content should be blurred
|
||||
if (shouldBlur) {
|
||||
@@ -172,6 +234,12 @@ class RecipeCard {
|
||||
this.clickHandler(this.recipe);
|
||||
});
|
||||
|
||||
// Favorite button click event - prevent propagation to card
|
||||
card.querySelector('.fa-star')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleFavorite(card);
|
||||
});
|
||||
|
||||
// Share button click event - prevent propagation to card
|
||||
card.querySelector('.fa-share-alt')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -225,6 +225,15 @@ class RecipeManager {
|
||||
if (bulkButton) {
|
||||
bulkButton.addEventListener('click', () => window.bulkManager?.toggleBulkMode());
|
||||
}
|
||||
|
||||
const favoriteFilterBtn = document.getElementById('favoriteFilterBtn');
|
||||
if (favoriteFilterBtn) {
|
||||
favoriteFilterBtn.addEventListener('click', () => {
|
||||
this.pageState.showFavoritesOnly = !this.pageState.showFavoritesOnly;
|
||||
favoriteFilterBtn.classList.toggle('active', this.pageState.showFavoritesOnly);
|
||||
refreshVirtualScroll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This method is kept for compatibility but now uses virtual scrolling
|
||||
|
||||
@@ -12,7 +12,7 @@ export class VirtualScroller {
|
||||
this.scrollContainer = options.scrollContainer || this.containerElement;
|
||||
this.batchSize = options.batchSize || 50;
|
||||
this.pageSize = options.pageSize || 100;
|
||||
this.itemAspectRatio = 896/1152; // Aspect ratio of cards
|
||||
this.itemAspectRatio = 896 / 1152; // Aspect ratio of cards
|
||||
this.rowGap = options.rowGap || 20; // Add vertical gap between rows (default 20px)
|
||||
|
||||
// Add container padding properties
|
||||
@@ -716,7 +716,7 @@ export class VirtualScroller {
|
||||
// Utility method for debouncing
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
return function (...args) {
|
||||
const context = this;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
@@ -783,31 +783,30 @@ export class VirtualScroller {
|
||||
deepMerge(target, source) {
|
||||
if (!source || !target) return target;
|
||||
|
||||
// Initialize result with a copy of target
|
||||
const result = { ...target };
|
||||
|
||||
// Only iterate over keys that exist in target
|
||||
Object.keys(target).forEach(key => {
|
||||
// Check if source has this key
|
||||
if (source.hasOwnProperty(key)) {
|
||||
const targetValue = target[key];
|
||||
const sourceValue = source[key];
|
||||
if (!source) return result;
|
||||
|
||||
// If both values are non-null objects and not arrays, merge recursively
|
||||
if (
|
||||
targetValue !== null &&
|
||||
typeof targetValue === 'object' &&
|
||||
!Array.isArray(targetValue) &&
|
||||
sourceValue !== null &&
|
||||
typeof sourceValue === 'object' &&
|
||||
!Array.isArray(sourceValue)
|
||||
) {
|
||||
result[key] = this.deepMerge(targetValue, sourceValue);
|
||||
} else {
|
||||
// For primitive types, arrays, or null, use the value from source
|
||||
result[key] = sourceValue;
|
||||
}
|
||||
// Iterate over all keys in the source object
|
||||
Object.keys(source).forEach(key => {
|
||||
const targetValue = target[key];
|
||||
const sourceValue = source[key];
|
||||
|
||||
// If both values are non-null objects and not arrays, merge recursively
|
||||
if (
|
||||
targetValue !== null &&
|
||||
typeof targetValue === 'object' &&
|
||||
!Array.isArray(targetValue) &&
|
||||
sourceValue !== null &&
|
||||
typeof sourceValue === 'object' &&
|
||||
!Array.isArray(sourceValue)
|
||||
) {
|
||||
result[key] = this.deepMerge(targetValue || {}, sourceValue);
|
||||
} else {
|
||||
// Otherwise update with source value (includes primitives, arrays, and new keys)
|
||||
result[key] = sourceValue;
|
||||
}
|
||||
// If source does not have this key, keep the original value from target
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
<button onclick="recipeManager.findDuplicateRecipes()"><i class="fas fa-clone"></i> {{
|
||||
t('loras.controls.duplicates.action') }}</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="favoriteFilterBtn" data-action="toggle-favorites" class="favorite-filter"
|
||||
title="{{ t('recipes.controls.favorites.title') }}">
|
||||
<i class="fas fa-star"></i> <span>{{ t('recipes.controls.favorites.action') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-group" title="{{ t('loras.controls.bulk.title') }}">
|
||||
<button id="bulkOperationsBtn" data-action="bulk" title="{{ t('loras.controls.bulk.title') }}">
|
||||
<i class="fas fa-th-large"></i> <span><span>{{ t('loras.controls.bulk.action') }}</span>
|
||||
|
||||
@@ -502,7 +502,50 @@ async def test_update_lora_filename_by_hash_updates_affected_recipes(tmp_path: P
|
||||
persisted2 = json.loads(recipe2_path.read_text())
|
||||
assert persisted2["loras"][0]["file_name"] == "other_lora"
|
||||
|
||||
# Check cache
|
||||
cache = await scanner.get_cached_data()
|
||||
cached1 = next(r for r in cache.raw_data if r["id"] == recipe1_id)
|
||||
assert cached1["loras"][0]["file_name"] == new_name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_filters_by_favorite(recipe_scanner):
|
||||
scanner, _ = recipe_scanner
|
||||
|
||||
# Add a normal recipe
|
||||
await scanner.add_recipe({
|
||||
"id": "regular",
|
||||
"file_path": "path/regular.png",
|
||||
"title": "Regular Recipe",
|
||||
"modified": 1.0,
|
||||
"created_date": 1.0,
|
||||
"loras": [],
|
||||
})
|
||||
|
||||
# Add a favorite recipe
|
||||
await scanner.add_recipe({
|
||||
"id": "favorite",
|
||||
"file_path": "path/favorite.png",
|
||||
"title": "Favorite Recipe",
|
||||
"modified": 2.0,
|
||||
"created_date": 2.0,
|
||||
"loras": [],
|
||||
"favorite": True
|
||||
})
|
||||
|
||||
# Wait for cache update (it's async in some places, add_recipe is usually enough but let's be safe)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# Test without filter (should return both)
|
||||
result_all = await scanner.get_paginated_data(page=1, page_size=10)
|
||||
assert len(result_all["items"]) == 2
|
||||
|
||||
# Test with favorite filter
|
||||
result_fav = await scanner.get_paginated_data(page=1, page_size=10, filters={"favorite": True})
|
||||
assert len(result_fav["items"]) == 1
|
||||
assert result_fav["items"][0]["id"] == "favorite"
|
||||
|
||||
# Test with favorite filter set to False (should return both or at least not filter if it's the default)
|
||||
# Actually our implementation checks if 'favorite' in filters and filters['favorite']
|
||||
result_fav_false = await scanner.get_paginated_data(page=1, page_size=10, filters={"favorite": False})
|
||||
assert len(result_fav_false["items"]) == 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user