mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: add folder-based recipe organization and navigation
- Add new API endpoints for folder operations: get_folders, get_folder_tree, and get_unified_folder_tree - Extend recipe listing handler to support folder and recursive filtering parameters - Register new folder-related routes in route definitions - Enable users to organize and browse recipes using folder structures
This commit is contained in:
@@ -56,6 +56,9 @@ class RecipeHandlerSet:
|
||||
"delete_recipe": self.management.delete_recipe,
|
||||
"get_top_tags": self.query.get_top_tags,
|
||||
"get_base_models": self.query.get_base_models,
|
||||
"get_folders": self.query.get_folders,
|
||||
"get_folder_tree": self.query.get_folder_tree,
|
||||
"get_unified_folder_tree": self.query.get_unified_folder_tree,
|
||||
"share_recipe": self.sharing.share_recipe,
|
||||
"download_shared_recipe": self.sharing.download_shared_recipe,
|
||||
"get_recipe_syntax": self.query.get_recipe_syntax,
|
||||
@@ -149,6 +152,8 @@ class RecipeListingHandler:
|
||||
page_size = int(request.query.get("page_size", "20"))
|
||||
sort_by = request.query.get("sort_by", "date")
|
||||
search = request.query.get("search")
|
||||
folder = request.query.get("folder")
|
||||
recursive = request.query.get("recursive", "true").lower() == "true"
|
||||
|
||||
search_options = {
|
||||
"title": request.query.get("search_title", "true").lower() == "true",
|
||||
@@ -193,6 +198,8 @@ class RecipeListingHandler:
|
||||
filters=filters,
|
||||
search_options=search_options,
|
||||
lora_hash=lora_hash,
|
||||
folder=folder,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
for item in result.get("items", []):
|
||||
@@ -299,6 +306,45 @@ class RecipeQueryHandler:
|
||||
self._logger.error("Error retrieving base models: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_folders(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
await self._ensure_dependencies_ready()
|
||||
recipe_scanner = self._recipe_scanner_getter()
|
||||
if recipe_scanner is None:
|
||||
raise RuntimeError("Recipe scanner unavailable")
|
||||
|
||||
folders = await recipe_scanner.get_folders()
|
||||
return web.json_response({"success": True, "folders": folders})
|
||||
except Exception as exc:
|
||||
self._logger.error("Error retrieving recipe folders: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_folder_tree(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
await self._ensure_dependencies_ready()
|
||||
recipe_scanner = self._recipe_scanner_getter()
|
||||
if recipe_scanner is None:
|
||||
raise RuntimeError("Recipe scanner unavailable")
|
||||
|
||||
folder_tree = await recipe_scanner.get_folder_tree()
|
||||
return web.json_response({"success": True, "tree": folder_tree})
|
||||
except Exception as exc:
|
||||
self._logger.error("Error retrieving recipe folder tree: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_unified_folder_tree(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
await self._ensure_dependencies_ready()
|
||||
recipe_scanner = self._recipe_scanner_getter()
|
||||
if recipe_scanner is None:
|
||||
raise RuntimeError("Recipe scanner unavailable")
|
||||
|
||||
folder_tree = await recipe_scanner.get_folder_tree()
|
||||
return web.json_response({"success": True, "tree": folder_tree})
|
||||
except Exception as exc:
|
||||
self._logger.error("Error retrieving unified recipe folder tree: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_recipes_for_lora(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
await self._ensure_dependencies_ready()
|
||||
|
||||
@@ -27,6 +27,9 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition("DELETE", "/api/lm/recipe/{recipe_id}", "delete_recipe"),
|
||||
RouteDefinition("GET", "/api/lm/recipes/top-tags", "get_top_tags"),
|
||||
RouteDefinition("GET", "/api/lm/recipes/base-models", "get_base_models"),
|
||||
RouteDefinition("GET", "/api/lm/recipes/folders", "get_folders"),
|
||||
RouteDefinition("GET", "/api/lm/recipes/folder-tree", "get_folder_tree"),
|
||||
RouteDefinition("GET", "/api/lm/recipes/unified-folder-tree", "get_unified_folder_tree"),
|
||||
RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/share", "share_recipe"),
|
||||
RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/share/download", "download_shared_recipe"),
|
||||
RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/syntax", "get_recipe_syntax"),
|
||||
|
||||
@@ -7,12 +7,18 @@ from natsort import natsorted
|
||||
@dataclass
|
||||
class RecipeCache:
|
||||
"""Cache structure for Recipe data"""
|
||||
|
||||
raw_data: List[Dict]
|
||||
sorted_by_name: List[Dict]
|
||||
sorted_by_date: List[Dict]
|
||||
folders: List[str] | None = None
|
||||
folder_tree: Dict | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
self._lock = asyncio.Lock()
|
||||
# Normalize optional metadata containers
|
||||
self.folders = self.folders or []
|
||||
self.folder_tree = self.folder_tree or {}
|
||||
|
||||
async def resort(self, name_only: bool = False):
|
||||
"""Resort all cached data views"""
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
import logging
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
||||
from ..config import config
|
||||
@@ -117,7 +119,9 @@ class RecipeScanner:
|
||||
self._cache = RecipeCache(
|
||||
raw_data=[],
|
||||
sorted_by_name=[],
|
||||
sorted_by_date=[]
|
||||
sorted_by_date=[],
|
||||
folders=[],
|
||||
folder_tree={},
|
||||
)
|
||||
|
||||
# Mark as initializing to prevent concurrent initializations
|
||||
@@ -218,6 +222,7 @@ class RecipeScanner:
|
||||
|
||||
# Update cache with the collected data
|
||||
self._cache.raw_data = recipes
|
||||
self._update_folder_metadata(self._cache)
|
||||
|
||||
# Create a simplified resort function that doesn't use await
|
||||
if hasattr(self._cache, "resort"):
|
||||
@@ -336,6 +341,9 @@ class RecipeScanner:
|
||||
if not self._cache:
|
||||
return
|
||||
|
||||
# Keep folder metadata up to date alongside sort order
|
||||
self._update_folder_metadata()
|
||||
|
||||
async def _resort_wrapper() -> None:
|
||||
try:
|
||||
await self._cache.resort(name_only=name_only)
|
||||
@@ -346,6 +354,75 @@ class RecipeScanner:
|
||||
self._resort_tasks.add(task)
|
||||
task.add_done_callback(lambda finished: self._resort_tasks.discard(finished))
|
||||
|
||||
def _calculate_folder(self, recipe_path: str) -> str:
|
||||
"""Calculate a normalized folder path relative to ``recipes_dir``."""
|
||||
|
||||
recipes_dir = self.recipes_dir
|
||||
if not recipes_dir:
|
||||
return ""
|
||||
|
||||
try:
|
||||
recipe_dir = os.path.dirname(os.path.normpath(recipe_path))
|
||||
relative_dir = os.path.relpath(recipe_dir, recipes_dir)
|
||||
if relative_dir in (".", ""):
|
||||
return ""
|
||||
return relative_dir.replace(os.path.sep, "/")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _build_folder_tree(self, folders: list[str]) -> dict:
|
||||
"""Build a nested folder tree structure from relative folder paths."""
|
||||
|
||||
tree: dict[str, dict] = {}
|
||||
for folder in folders:
|
||||
if not folder:
|
||||
continue
|
||||
|
||||
parts = folder.split("/")
|
||||
current_level = tree
|
||||
|
||||
for part in parts:
|
||||
if part not in current_level:
|
||||
current_level[part] = {}
|
||||
current_level = current_level[part]
|
||||
|
||||
return tree
|
||||
|
||||
def _update_folder_metadata(self, cache: RecipeCache | None = None) -> None:
|
||||
"""Ensure folder lists and tree metadata are synchronized with cache contents."""
|
||||
|
||||
cache = cache or self._cache
|
||||
if cache is None:
|
||||
return
|
||||
|
||||
folders: set[str] = set()
|
||||
for item in cache.raw_data:
|
||||
folder_value = item.get("folder", "")
|
||||
if folder_value is None:
|
||||
folder_value = ""
|
||||
if folder_value == ".":
|
||||
folder_value = ""
|
||||
normalized = str(folder_value).replace("\\", "/")
|
||||
item["folder"] = normalized
|
||||
folders.add(normalized)
|
||||
|
||||
cache.folders = sorted(folders, key=lambda entry: entry.lower())
|
||||
cache.folder_tree = self._build_folder_tree(cache.folders)
|
||||
|
||||
async def get_folders(self) -> list[str]:
|
||||
"""Return a sorted list of recipe folders relative to the recipes root."""
|
||||
|
||||
cache = await self.get_cached_data()
|
||||
self._update_folder_metadata(cache)
|
||||
return cache.folders
|
||||
|
||||
async def get_folder_tree(self) -> dict:
|
||||
"""Return a hierarchical tree of recipe folders for sidebar navigation."""
|
||||
|
||||
cache = await self.get_cached_data()
|
||||
self._update_folder_metadata(cache)
|
||||
return cache.folder_tree
|
||||
|
||||
@property
|
||||
def recipes_dir(self) -> str:
|
||||
"""Get path to recipes directory"""
|
||||
@@ -362,11 +439,14 @@ class RecipeScanner:
|
||||
"""Get cached recipe data, refresh if needed"""
|
||||
# If cache is already initialized and no refresh is needed, return it immediately
|
||||
if self._cache is not None and not force_refresh:
|
||||
self._update_folder_metadata()
|
||||
return self._cache
|
||||
|
||||
# If another initialization is already in progress, wait for it to complete
|
||||
if self._is_initializing and not force_refresh:
|
||||
return self._cache or RecipeCache(raw_data=[], sorted_by_name=[], sorted_by_date=[])
|
||||
return self._cache or RecipeCache(
|
||||
raw_data=[], sorted_by_name=[], sorted_by_date=[], folders=[], folder_tree={}
|
||||
)
|
||||
|
||||
# If force refresh is requested, initialize the cache directly
|
||||
if force_refresh:
|
||||
@@ -384,11 +464,14 @@ class RecipeScanner:
|
||||
self._cache = RecipeCache(
|
||||
raw_data=raw_data,
|
||||
sorted_by_name=[],
|
||||
sorted_by_date=[]
|
||||
sorted_by_date=[],
|
||||
folders=[],
|
||||
folder_tree={},
|
||||
)
|
||||
|
||||
|
||||
# Resort cache
|
||||
await self._cache.resort()
|
||||
self._update_folder_metadata(self._cache)
|
||||
|
||||
return self._cache
|
||||
|
||||
@@ -398,7 +481,9 @@ class RecipeScanner:
|
||||
self._cache = RecipeCache(
|
||||
raw_data=[],
|
||||
sorted_by_name=[],
|
||||
sorted_by_date=[]
|
||||
sorted_by_date=[],
|
||||
folders=[],
|
||||
folder_tree={},
|
||||
)
|
||||
return self._cache
|
||||
finally:
|
||||
@@ -409,7 +494,9 @@ class RecipeScanner:
|
||||
logger.error(f"Unexpected error in get_cached_data: {e}")
|
||||
|
||||
# Return the cache (may be empty or partially initialized)
|
||||
return self._cache or RecipeCache(raw_data=[], sorted_by_name=[], sorted_by_date=[])
|
||||
return self._cache or RecipeCache(
|
||||
raw_data=[], sorted_by_name=[], sorted_by_date=[], folders=[], folder_tree={}
|
||||
)
|
||||
|
||||
async def refresh_cache(self, force: bool = False) -> RecipeCache:
|
||||
"""Public helper to refresh or return the recipe cache."""
|
||||
@@ -424,6 +511,7 @@ class RecipeScanner:
|
||||
|
||||
cache = await self.get_cached_data()
|
||||
await cache.add_recipe(recipe_data, resort=False)
|
||||
self._update_folder_metadata(cache)
|
||||
self._schedule_resort()
|
||||
|
||||
async def remove_recipe(self, recipe_id: str) -> bool:
|
||||
@@ -437,6 +525,7 @@ class RecipeScanner:
|
||||
if removed is None:
|
||||
return False
|
||||
|
||||
self._update_folder_metadata(cache)
|
||||
self._schedule_resort()
|
||||
return True
|
||||
|
||||
@@ -521,6 +610,9 @@ class RecipeScanner:
|
||||
|
||||
if path_updated:
|
||||
self._write_recipe_file(recipe_path, recipe_data)
|
||||
|
||||
# Track folder placement relative to recipes directory
|
||||
recipe_data['folder'] = recipe_data.get('folder') or self._calculate_folder(recipe_path)
|
||||
|
||||
# Ensure loras array exists
|
||||
if 'loras' not in recipe_data:
|
||||
@@ -914,7 +1006,7 @@ class RecipeScanner:
|
||||
|
||||
return await self._lora_scanner.get_model_info_by_name(name)
|
||||
|
||||
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'date', search: str = None, filters: dict = None, search_options: dict = None, lora_hash: str = None, bypass_filters: bool = True):
|
||||
async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'date', search: str = None, filters: dict = None, search_options: dict = None, lora_hash: str = None, bypass_filters: bool = True, folder: str | None = None, recursive: bool = True):
|
||||
"""Get paginated and filtered recipe data
|
||||
|
||||
Args:
|
||||
@@ -926,6 +1018,8 @@ class RecipeScanner:
|
||||
search_options: Dictionary of search options to apply
|
||||
lora_hash: Optional SHA256 hash of a LoRA to filter recipes by
|
||||
bypass_filters: If True, ignore other filters when a lora_hash is provided
|
||||
folder: Optional folder filter relative to recipes directory
|
||||
recursive: Whether to include recipes in subfolders of the selected folder
|
||||
"""
|
||||
cache = await self.get_cached_data()
|
||||
|
||||
@@ -961,6 +1055,22 @@ class RecipeScanner:
|
||||
|
||||
# Skip further filtering if we're only filtering by LoRA hash with bypass enabled
|
||||
if not (lora_hash and bypass_filters):
|
||||
# Apply folder filter before other criteria
|
||||
normalized_folder = (folder or "").strip("/")
|
||||
if normalized_folder:
|
||||
def matches_folder(item_folder: str) -> bool:
|
||||
item_path = (item_folder or "").strip("/")
|
||||
if not item_path:
|
||||
return False
|
||||
if recursive:
|
||||
return item_path == normalized_folder or item_path.startswith(f"{normalized_folder}/")
|
||||
return item_path == normalized_folder
|
||||
|
||||
filtered_data = [
|
||||
item for item in filtered_data
|
||||
if matches_folder(item.get('folder', ''))
|
||||
]
|
||||
|
||||
# Apply search filter
|
||||
if search:
|
||||
# Default search options if none provided
|
||||
|
||||
@@ -2,6 +2,24 @@ import { RecipeCard } from '../components/RecipeCard.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
const RECIPE_ENDPOINTS = {
|
||||
list: '/api/lm/recipes',
|
||||
detail: '/api/lm/recipe',
|
||||
scan: '/api/lm/recipes/scan',
|
||||
update: '/api/lm/recipe',
|
||||
folders: '/api/lm/recipes/folders',
|
||||
folderTree: '/api/lm/recipes/folder-tree',
|
||||
unifiedFolderTree: '/api/lm/recipes/unified-folder-tree',
|
||||
};
|
||||
|
||||
const RECIPE_SIDEBAR_CONFIG = {
|
||||
config: {
|
||||
displayName: 'Recipes',
|
||||
supportsMove: false,
|
||||
},
|
||||
endpoints: RECIPE_ENDPOINTS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch recipes with pagination for virtual scrolling
|
||||
* @param {number} page - Page number to fetch
|
||||
@@ -17,11 +35,18 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
page_size: pageSize || pageState.pageSize || 20,
|
||||
sort_by: pageState.sortBy
|
||||
});
|
||||
|
||||
if (pageState.activeFolder) {
|
||||
params.append('folder', pageState.activeFolder);
|
||||
params.append('recursive', pageState.searchOptions?.recursive !== false);
|
||||
} else if (pageState.searchOptions?.recursive !== undefined) {
|
||||
params.append('recursive', pageState.searchOptions.recursive);
|
||||
}
|
||||
|
||||
// If we have a specific recipe ID to load
|
||||
if (pageState.customFilter?.active && pageState.customFilter?.recipeId) {
|
||||
// Special case: load specific recipe
|
||||
const response = await fetch(`/api/lm/recipe/${pageState.customFilter.recipeId}`);
|
||||
const response = await fetch(`${RECIPE_ENDPOINTS.detail}/${pageState.customFilter.recipeId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load recipe: ${response.statusText}`);
|
||||
@@ -78,7 +103,7 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
||||
}
|
||||
|
||||
// Fetch recipes
|
||||
const response = await fetch(`/api/lm/recipes?${params.toString()}`);
|
||||
const response = await fetch(`${RECIPE_ENDPOINTS.list}?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load recipes: ${response.statusText}`);
|
||||
@@ -213,7 +238,7 @@ export async function refreshRecipes() {
|
||||
state.loadingManager.showSimpleLoading('Refreshing recipes...');
|
||||
|
||||
// Call the API endpoint to rebuild the recipe cache
|
||||
const response = await fetch('/api/lm/recipes/scan');
|
||||
const response = await fetch(RECIPE_ENDPOINTS.scan);
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
@@ -280,7 +305,7 @@ export async function updateRecipeMetadata(filePath, updates) {
|
||||
const basename = filePath.split('/').pop().split('\\').pop();
|
||||
const recipeId = basename.substring(0, basename.lastIndexOf('.'));
|
||||
|
||||
const response = await fetch(`/api/lm/recipe/${recipeId}/update`, {
|
||||
const response = await fetch(`${RECIPE_ENDPOINTS.update}/${recipeId}/update`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -306,3 +331,33 @@ export async function updateRecipeMetadata(filePath, updates) {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
export class RecipeSidebarApiClient {
|
||||
constructor() {
|
||||
this.apiConfig = RECIPE_SIDEBAR_CONFIG;
|
||||
}
|
||||
|
||||
async fetchUnifiedFolderTree() {
|
||||
const response = await fetch(this.apiConfig.endpoints.unifiedFolderTree);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch recipe folder tree');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async fetchModelFolders() {
|
||||
const response = await fetch(this.apiConfig.endpoints.folders);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch recipe folders');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async moveBulkModels() {
|
||||
throw new Error('Recipe move operations are not supported.');
|
||||
}
|
||||
|
||||
async moveSingleModel() {
|
||||
throw new Error('Recipe move operations are not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ export class SidebarManager {
|
||||
this.pageControls = pageControls;
|
||||
this.pageType = pageControls.pageType;
|
||||
this.lastPageControls = pageControls;
|
||||
this.apiClient = getModelApiClient();
|
||||
this.apiClient = pageControls?.getSidebarApiClient?.()
|
||||
|| pageControls?.sidebarApiClient
|
||||
|| getModelApiClient();
|
||||
|
||||
// Set initial sidebar state immediately (hidden by default)
|
||||
this.setInitialSidebarState();
|
||||
@@ -205,6 +207,10 @@ export class SidebarManager {
|
||||
}
|
||||
|
||||
initializeDragAndDrop() {
|
||||
if (this.apiClient?.apiConfig?.config?.supportsMove === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dragHandlersInitialized) {
|
||||
document.addEventListener('dragstart', this.handleCardDragStart);
|
||||
document.addEventListener('dragend', this.handleCardDragEnd);
|
||||
@@ -416,7 +422,14 @@ export class SidebarManager {
|
||||
}
|
||||
|
||||
if (!this.apiClient) {
|
||||
this.apiClient = getModelApiClient();
|
||||
this.apiClient = this.pageControls?.getSidebarApiClient?.()
|
||||
|| this.pageControls?.sidebarApiClient
|
||||
|| getModelApiClient();
|
||||
}
|
||||
|
||||
if (this.apiClient?.apiConfig?.config?.supportsMove === false) {
|
||||
showToast('toast.models.moveFailed', { message: translate('sidebar.dragDrop.moveUnsupported', {}, 'Move not supported for this page') }, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootPath = this.draggedRootPath ? this.draggedRootPath.replace(/\\/g, '/') : '';
|
||||
@@ -470,7 +483,9 @@ export class SidebarManager {
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.apiClient = getModelApiClient();
|
||||
this.apiClient = this.pageControls?.getSidebarApiClient?.()
|
||||
|| this.pageControls?.sidebarApiClient
|
||||
|| getModelApiClient();
|
||||
|
||||
// Set initial sidebar state immediately (hidden by default)
|
||||
this.setInitialSidebarState();
|
||||
|
||||
@@ -2,17 +2,46 @@
|
||||
import { appCore } from './core.js';
|
||||
import { ImportManager } from './managers/ImportManager.js';
|
||||
import { RecipeModal } from './components/RecipeModal.js';
|
||||
import { getCurrentPageState } from './state/index.js';
|
||||
import { state, getCurrentPageState } from './state/index.js';
|
||||
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 } from './api/recipeApi.js';
|
||||
import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js';
|
||||
import { sidebarManager } from './components/SidebarManager.js';
|
||||
|
||||
class RecipePageControls {
|
||||
constructor() {
|
||||
this.pageType = 'recipes';
|
||||
this.pageState = getCurrentPageState();
|
||||
this.sidebarApiClient = new RecipeSidebarApiClient();
|
||||
}
|
||||
|
||||
async resetAndReload() {
|
||||
refreshVirtualScroll();
|
||||
}
|
||||
|
||||
async refreshModels(fullRebuild = false) {
|
||||
if (fullRebuild) {
|
||||
await refreshRecipes();
|
||||
return;
|
||||
}
|
||||
|
||||
refreshVirtualScroll();
|
||||
}
|
||||
|
||||
getSidebarApiClient() {
|
||||
return this.sidebarApiClient;
|
||||
}
|
||||
}
|
||||
|
||||
class RecipeManager {
|
||||
constructor() {
|
||||
// Get page state
|
||||
this.pageState = getCurrentPageState();
|
||||
|
||||
// Page controls for shared sidebar behaviors
|
||||
this.pageControls = new RecipePageControls();
|
||||
|
||||
// Initialize ImportManager
|
||||
this.importManager = new ImportManager();
|
||||
@@ -51,10 +80,23 @@ class RecipeManager {
|
||||
|
||||
// Expose necessary functions to the page
|
||||
this._exposeGlobalFunctions();
|
||||
|
||||
// Initialize sidebar navigation
|
||||
await this._initSidebar();
|
||||
|
||||
// Initialize common page features
|
||||
appCore.initializePageFeatures();
|
||||
}
|
||||
|
||||
async _initSidebar() {
|
||||
try {
|
||||
sidebarManager.setHostPageControls(this.pageControls);
|
||||
const shouldShowSidebar = state?.global?.settings?.show_folder_sidebar !== false;
|
||||
await sidebarManager.setSidebarEnabled(shouldShowSidebar);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize recipe sidebar:', error);
|
||||
}
|
||||
}
|
||||
|
||||
_initSearchOptions() {
|
||||
// Ensure recipes search options are properly initialized
|
||||
@@ -63,7 +105,8 @@ class RecipeManager {
|
||||
title: true, // Recipe title
|
||||
tags: true, // Recipe tags
|
||||
loraName: true, // LoRA file name
|
||||
loraModel: true // LoRA model name
|
||||
loraModel: true, // LoRA model name
|
||||
recursive: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +96,14 @@ export const state = {
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
sortBy: 'date',
|
||||
activeFolder: getStorageItem('recipes_activeFolder'),
|
||||
searchManager: null,
|
||||
searchOptions: {
|
||||
title: true,
|
||||
tags: true,
|
||||
loraName: true,
|
||||
loraModel: true
|
||||
loraModel: true,
|
||||
recursive: getStorageItem('recipes_recursiveSearch', true),
|
||||
},
|
||||
filters: {
|
||||
baseModel: [],
|
||||
|
||||
@@ -75,7 +75,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% include 'components/folder_sidebar.html' %}
|
||||
|
||||
<!-- Recipe grid -->
|
||||
<div class="card-grid" id="recipeGrid">
|
||||
<!-- Remove the server-side conditional rendering and placeholder -->
|
||||
|
||||
Reference in New Issue
Block a user