mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-22 11:21:15 -03:00
Compare commits
6 Commits
ccf1c6f2ae
...
df67bd396a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df67bd396a | ||
|
|
dd5d9cfcb2 | ||
|
|
d9fd60bec1 | ||
|
|
b633b22779 | ||
|
|
1ffa543160 | ||
|
|
cdc940586e |
@@ -461,7 +461,11 @@ class RecipeQueryHandler:
|
||||
if recipe_scanner is None:
|
||||
raise RuntimeError("Recipe scanner unavailable")
|
||||
|
||||
self._logger.info("Manually triggering recipe cache rebuild")
|
||||
full_rebuild = request.query.get("full_rebuild", "true").lower() == "true"
|
||||
self._logger.info(
|
||||
"Manually triggering recipe cache %s",
|
||||
"full rebuild" if full_rebuild else "refresh",
|
||||
)
|
||||
await recipe_scanner.get_cached_data(force_refresh=True)
|
||||
return web.json_response(
|
||||
{"success": True, "message": "Recipe cache refreshed successfully"}
|
||||
|
||||
@@ -186,6 +186,22 @@ class CivArchiveClient:
|
||||
if "metadata" in file_data:
|
||||
transformed["metadata"] = file_data["metadata"]
|
||||
|
||||
# Infer metadata.format from filename extension
|
||||
name = transformed.get("name")
|
||||
if name and isinstance(name, str):
|
||||
lower_name = name.lower()
|
||||
if lower_name.endswith(".safetensors"):
|
||||
inferred_format = "SafeTensor"
|
||||
elif lower_name.endswith(".ckpt"):
|
||||
inferred_format = "PickleTensor"
|
||||
else:
|
||||
inferred_format = None
|
||||
if inferred_format:
|
||||
if "metadata" not in transformed:
|
||||
transformed["metadata"] = {}
|
||||
if isinstance(transformed["metadata"], dict):
|
||||
transformed["metadata"].setdefault("format", inferred_format)
|
||||
|
||||
if file_data.get("modelVersionId") is not None:
|
||||
transformed["modelVersionId"] = file_data.get("modelVersionId")
|
||||
elif file_data.get("model_version_id") is not None:
|
||||
@@ -213,6 +229,20 @@ class CivArchiveClient:
|
||||
for file_data in candidates:
|
||||
if isinstance(file_data, dict):
|
||||
transformed_files.append(self._transform_file_entry(file_data))
|
||||
|
||||
# Sort: .safetensors first, .ckpt second, others last
|
||||
# so the backend fallback (no file_params) prefers safetensors
|
||||
def _sort_key(f: Dict) -> int:
|
||||
fname = f.get("name") or ""
|
||||
if isinstance(fname, str):
|
||||
lower = fname.lower()
|
||||
if lower.endswith(".safetensors"):
|
||||
return 0
|
||||
elif lower.endswith(".ckpt"):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
transformed_files.sort(key=_sort_key)
|
||||
return transformed_files
|
||||
|
||||
def _transform_version(
|
||||
|
||||
@@ -197,8 +197,8 @@ export async function resetAndReloadWithVirtualScroll(options = {}) {
|
||||
// Reset page counter
|
||||
pageState.currentPage = 1;
|
||||
|
||||
// Fetch the first page
|
||||
const result = await fetchPageFunction(1, pageState.pageSize || 50);
|
||||
const pageSize = state.virtualScroller?.pageSize || pageState.pageSize || 100;
|
||||
const result = await fetchPageFunction(1, pageSize);
|
||||
|
||||
// Update the virtual scroller
|
||||
state.virtualScroller.refreshWithData(
|
||||
@@ -251,8 +251,8 @@ export async function loadMoreWithVirtualScroll(options = {}) {
|
||||
pageState.currentPage = 1;
|
||||
}
|
||||
|
||||
// Fetch the first page of data
|
||||
const result = await fetchPageFunction(pageState.currentPage, pageState.pageSize || 50);
|
||||
const pageSize = state.virtualScroller?.pageSize || pageState.pageSize || 100;
|
||||
const result = await fetchPageFunction(pageState.currentPage, pageSize);
|
||||
|
||||
// Update virtual scroller with the new data
|
||||
state.virtualScroller.refreshWithData(
|
||||
@@ -294,47 +294,41 @@ export async function resetAndReload(updateFolders = false, options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync changes - quick refresh without rebuilding cache (similar to models page)
|
||||
* Refreshes the recipe list by triggering a backend scan, then reloading.
|
||||
* @param {boolean} fullRebuild - If true, fully rebuild the cache; if false, incremental scan
|
||||
*/
|
||||
export async function syncChanges() {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Syncing changes...');
|
||||
|
||||
// Simply reload the recipes without rebuilding cache
|
||||
await resetAndReload(false, { preserveScroll: true });
|
||||
|
||||
showToast('toast.recipes.syncComplete', {}, 'success');
|
||||
} catch (error) {
|
||||
console.error('Error syncing recipes:', error);
|
||||
showToast('toast.recipes.syncFailed', { message: error.message }, 'error');
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
return refreshRecipes(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the recipe list by first rebuilding the cache and then loading recipes
|
||||
*/
|
||||
export async function refreshRecipes() {
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Refreshing recipes...');
|
||||
export async function refreshRecipes(fullRebuild = true) {
|
||||
const actionLabel = fullRebuild ? 'Rebuilding recipe cache' : 'Refreshing recipes';
|
||||
const actionToast = fullRebuild ? 'Full rebuild' : 'Refresh';
|
||||
|
||||
// Call the API endpoint to rebuild the recipe cache
|
||||
const response = await fetch(RECIPE_ENDPOINTS.scan);
|
||||
try {
|
||||
state.loadingManager.show(`${actionLabel}...`, 0);
|
||||
|
||||
const url = new URL(RECIPE_ENDPOINTS.scan, window.location.origin);
|
||||
url.searchParams.append('full_rebuild', fullRebuild);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to refresh recipe cache');
|
||||
throw new Error(`Failed to refresh recipe cache: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// After successful cache rebuild, reload the recipes
|
||||
await resetAndReload(false, { preserveScroll: true });
|
||||
const data = await response.json();
|
||||
if (data.status === 'cancelled') {
|
||||
showToast('toast.api.operationCancelled', {}, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast('toast.recipes.refreshComplete', {}, 'success');
|
||||
await resetAndReload(false);
|
||||
|
||||
showToast('toast.api.refreshComplete', { action: actionToast }, 'success');
|
||||
} catch (error) {
|
||||
console.error('Error refreshing recipes:', error);
|
||||
showToast('toast.recipes.refreshFailed', { message: error.message }, 'error');
|
||||
showToast('toast.api.refreshFailed', { action: fullRebuild ? 'rebuild' : 'refresh', type: 'recipe' }, 'error');
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
state.loadingManager.restoreProgressBar();
|
||||
|
||||
@@ -306,8 +306,14 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
if (result.success) {
|
||||
if (result.repaired > 0) {
|
||||
showToast('recipes.contextMenu.repair.success', {}, 'success');
|
||||
// Refresh the current card or reload
|
||||
this.resetAndReload();
|
||||
const detailResponse = await fetch(`/api/lm/recipe/${recipeId}`);
|
||||
if (detailResponse.ok) {
|
||||
const updatedRecipe = await detailResponse.json();
|
||||
const filePath = this.currentCard?.dataset?.filepath;
|
||||
if (filePath && state.virtualScroller) {
|
||||
state.virtualScroller.updateSingleItem(filePath, updatedRecipe);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast('recipes.contextMenu.repair.skipped', {}, 'info');
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class RecipeCard {
|
||||
card.dataset.created = this.recipe.created_date;
|
||||
card.dataset.id = this.recipe.id || '';
|
||||
card.dataset.folder = this.recipe.folder || '';
|
||||
card.dataset.favorite = this.recipe.favorite ? 'true' : 'false';
|
||||
|
||||
// Get base model with fallback
|
||||
const baseModelLabel = (this.recipe.base_model || '').trim() || 'Unknown';
|
||||
@@ -161,6 +162,7 @@ class RecipeCard {
|
||||
|
||||
// Update early to provide instant feedback and avoid race conditions with re-renders
|
||||
this.recipe.favorite = newFavoriteState;
|
||||
card.dataset.favorite = newFavoriteState ? 'true' : 'false';
|
||||
|
||||
// Function to update icon state
|
||||
const updateIconUI = (icon, state) => {
|
||||
|
||||
@@ -432,7 +432,7 @@ export class BatchImportManager {
|
||||
|
||||
// Refresh recipes list to show newly imported recipes
|
||||
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
|
||||
window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
window.recipeManager.loadRecipes(true);
|
||||
}
|
||||
|
||||
// Show results step
|
||||
|
||||
@@ -309,9 +309,22 @@ export class BulkMissingLoraDownloadManager {
|
||||
}, 'warning');
|
||||
}
|
||||
|
||||
// Refresh the recipes list to update LoRA status
|
||||
if (window.recipeManager) {
|
||||
window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
// Update each affected recipe card with fresh data (LoRA inLibrary flags changed)
|
||||
if (state.virtualScroller) {
|
||||
const { extractRecipeId } = await import('../api/recipeApi.js');
|
||||
for (const recipe of this.pendingRecipes) {
|
||||
const recipeId = extractRecipeId(recipe.file_path);
|
||||
if (!recipeId) continue;
|
||||
try {
|
||||
const detailRes = await fetch(`/api/lm/recipe/${encodeURIComponent(recipeId)}`);
|
||||
if (detailRes.ok) {
|
||||
const updated = await detailRes.json();
|
||||
state.virtualScroller.updateSingleItem(recipe.file_path, updated);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to update recipe card after LoRA download:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ export class FilterManager {
|
||||
|
||||
// Call the appropriate manager's load method based on page type
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
await window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
await window.recipeManager.loadRecipes(true);
|
||||
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings' || this.currentPage === 'checkpoints') {
|
||||
// For models page, reset the page and reload
|
||||
await getModelApiClient().loadMoreWithVirtualScroll(true, false);
|
||||
@@ -746,7 +746,7 @@ export class FilterManager {
|
||||
|
||||
// Reload data using the appropriate method for the current page
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
await window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
await window.recipeManager.loadRecipes(true);
|
||||
} else if (this.currentPage === 'loras' || this.currentPage === 'checkpoints' || this.currentPage === 'embeddings') {
|
||||
await getModelApiClient().loadMoreWithVirtualScroll(true, true);
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ export class SearchManager {
|
||||
|
||||
// Call the appropriate manager's load method based on page type
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
window.recipeManager.loadRecipes(true);
|
||||
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings' || this.currentPage === 'checkpoints') {
|
||||
// For models page, reset the page and reload
|
||||
getModelApiClient().loadMoreWithVirtualScroll(true, false);
|
||||
|
||||
@@ -2876,7 +2876,7 @@ export class SettingsManager {
|
||||
await resetAndReload(false);
|
||||
} else if (this.currentPage === 'recipes') {
|
||||
// Reload the recipes without updating folders
|
||||
await window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
await window.recipeManager.loadRecipes(true);
|
||||
} else if (this.currentPage === 'checkpoints') {
|
||||
// Reload the checkpoints without updating folders
|
||||
await resetAndReload(false);
|
||||
|
||||
@@ -122,7 +122,7 @@ export class DownloadManager {
|
||||
modalManager.closeModal('importModal');
|
||||
|
||||
// Refresh the recipe
|
||||
window.recipeManager.loadRecipes({ preserveScroll: true });
|
||||
window.recipeManager.loadRecipes(true);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
|
||||
import { RecipeContextMenu } from './components/ContextMenu/index.js';
|
||||
import { DuplicatesManager } from './components/DuplicatesManager.js';
|
||||
import { refreshVirtualScroll } from './utils/infiniteScroll.js';
|
||||
import { refreshRecipes, syncChanges, RecipeSidebarApiClient } from './api/recipeApi.js';
|
||||
import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js';
|
||||
import { sidebarManager } from './components/SidebarManager.js';
|
||||
|
||||
class RecipePageControls {
|
||||
@@ -19,16 +19,13 @@ class RecipePageControls {
|
||||
}
|
||||
|
||||
async resetAndReload() {
|
||||
await refreshVirtualScroll({ preserveScroll: true });
|
||||
await refreshVirtualScroll();
|
||||
}
|
||||
|
||||
async refreshModels(fullRebuild = false) {
|
||||
if (fullRebuild) {
|
||||
await refreshRecipes();
|
||||
return;
|
||||
}
|
||||
await refreshRecipes(fullRebuild);
|
||||
|
||||
await syncChanges();
|
||||
await sidebarManager.refresh();
|
||||
}
|
||||
|
||||
getSidebarApiClient() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||
const showToastMock = vi.hoisted(() => vi.fn());
|
||||
const loadingManagerMock = vi.hoisted(() => ({
|
||||
showSimpleLoading: vi.fn(),
|
||||
show: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
restoreProgressBar: vi.fn(),
|
||||
}));
|
||||
@@ -177,9 +178,7 @@ describe('RecipeSidebarApiClient bulk operations', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves scroll position for recipe reloads when requested', async () => {
|
||||
const scrollSnapshot = { scrollContainer: { scrollTop: 480 }, scrollTop: 480 };
|
||||
captureScrollPositionMock.mockReturnValue(scrollSnapshot);
|
||||
it('reloads recipes without preserving scroll', async () => {
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -189,18 +188,18 @@ describe('RecipeSidebarApiClient bulk operations', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
await resetAndReload(false, { preserveScroll: true });
|
||||
await resetAndReload(false);
|
||||
|
||||
expect(captureScrollPositionMock).toHaveBeenCalledTimes(1);
|
||||
expect(captureScrollPositionMock).not.toHaveBeenCalled();
|
||||
expect(virtualScrollerMock.refreshWithData).toHaveBeenCalledWith(
|
||||
[{ id: 'recipe-1' }],
|
||||
1,
|
||||
false
|
||||
);
|
||||
expect(restoreScrollPositionMock).toHaveBeenCalledWith(scrollSnapshot);
|
||||
expect(restoreScrollPositionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses scroll-preserving reloads for syncChanges', async () => {
|
||||
it('uses scroll-free reloads for syncChanges', async () => {
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -212,8 +211,8 @@ describe('RecipeSidebarApiClient bulk operations', () => {
|
||||
|
||||
await syncChanges();
|
||||
|
||||
expect(captureScrollPositionMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreScrollPositionMock).toHaveBeenCalledTimes(1);
|
||||
expect(captureScrollPositionMock).not.toHaveBeenCalled();
|
||||
expect(restoreScrollPositionMock).not.toHaveBeenCalled();
|
||||
expect(loadingManagerMock.restoreProgressBar).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user