mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
373 lines
15 KiB
HTML
373 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>LoRA Recipes</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="stylesheet" href="/loras_static/css/style.css">
|
|
<link rel="stylesheet" href="/loras_static/css/components/recipe-card.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
|
|
<link rel="manifest" href="/loras_static/images/site.webmanifest">
|
|
|
|
<!-- Preload critical resources -->
|
|
<link rel="preload" href="/loras_static/css/style.css" as="style">
|
|
<link rel="preload" href="/loras_static/js/recipes.js" as="script" crossorigin="anonymous">
|
|
|
|
<!-- Optimize font loading -->
|
|
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
|
|
|
|
<!-- Performance monitoring -->
|
|
<script>
|
|
performance.mark('page-start');
|
|
window.addEventListener('load', () => {
|
|
performance.mark('page-end');
|
|
performance.measure('page-load', 'page-start', 'page-end');
|
|
});
|
|
</script>
|
|
|
|
<!-- Security meta tags -->
|
|
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
|
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
|
|
|
<!-- Resource loading strategy -->
|
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
|
|
|
<style>
|
|
/* Recipe-specific styles */
|
|
.recipe-tag-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.recipe-tag {
|
|
background: var(--lora-surface-hover);
|
|
color: var(--lora-text-secondary);
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: var(--border-radius-sm);
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.recipe-tag:hover, .recipe-tag.active {
|
|
background: var(--lora-primary);
|
|
color: var(--lora-text-on-primary);
|
|
}
|
|
|
|
.recipe-card {
|
|
position: relative;
|
|
background: var(--lora-surface);
|
|
border-radius: var(--border-radius-base);
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: all 0.2s ease;
|
|
cursor: pointer;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.recipe-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.recipe-indicator {
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 8px;
|
|
width: 24px;
|
|
height: 24px;
|
|
background: var(--lora-primary);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
z-index: 2;
|
|
}
|
|
|
|
.recipe-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.placeholder-message {
|
|
grid-column: 1 / -1;
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: var(--lora-surface-alt);
|
|
border-radius: var(--border-radius-base);
|
|
}
|
|
|
|
.recipes-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.recipes-controls {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="corner-controls">
|
|
<div class="corner-controls-toggle">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
<span class="update-badge corner-badge hidden"></span>
|
|
</div>
|
|
<div class="corner-controls-items">
|
|
<div class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
|
|
<img src="/loras_static/images/theme-toggle-light.svg" alt="Theme" class="theme-icon light-icon">
|
|
<img src="/loras_static/images/theme-toggle-dark.svg" alt="Theme" class="theme-icon dark-icon">
|
|
</div>
|
|
<div class="update-toggle" id="updateToggleBtn" title="Check Updates">
|
|
<i class="fas fa-bell"></i>
|
|
<span class="update-badge hidden"></span>
|
|
</div>
|
|
<div class="support-toggle" id="supportToggleBtn" title="Support">
|
|
<i class="fas fa-heart"></i>
|
|
</div>
|
|
<div class="settings-toggle" onclick="settingsManager.toggleSettings()" title="Settings">
|
|
<i class="fas fa-cog"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% include 'components/modals.html' %}
|
|
{% include 'components/loading.html' %}
|
|
{% include 'components/context_menu.html' %}
|
|
|
|
<div class="container">
|
|
{% if is_initializing %}
|
|
<div class="initialization-notice">
|
|
<div class="notice-content">
|
|
<div class="loading-spinner"></div>
|
|
<h2>Initializing Recipe Manager</h2>
|
|
<p>Scanning and building recipe cache. This may take a few moments...</p>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="recipes-header">
|
|
<h1>LoRA Recipes</h1>
|
|
<div class="recipes-controls">
|
|
<a href="/loras" class="button"><i class="fas fa-arrow-left"></i> Back to LoRAs</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recipe controls -->
|
|
<div class="controls">
|
|
<div class="folder-tags-container">
|
|
<div class="recipe-tag-container">
|
|
{% if recipe_tags %}
|
|
{% for tag in recipe_tags %}
|
|
<div class="recipe-tag" data-tag="{{ tag }}">{{ tag }}</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="recipe-tag" data-tag="all">All</div>
|
|
{% endif %}
|
|
</div>
|
|
<button class="toggle-folders-btn" onclick="toggleTagsContainer()" title="Collapse tags">
|
|
<i class="fas fa-chevron-up"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<div title="Sort recipes by..." class="control-group">
|
|
<select id="sortSelect">
|
|
<option value="date">Date</option>
|
|
<option value="name">Name</option>
|
|
</select>
|
|
</div>
|
|
<div title="Refresh recipes list" class="control-group">
|
|
<button onclick="refreshRecipes()"><i class="fas fa-sync"></i> Refresh</button>
|
|
</div>
|
|
<div class="search-container">
|
|
<input type="text" id="searchInput" placeholder="Search recipes..." />
|
|
<i class="fas fa-search search-icon"></i>
|
|
<button class="search-filter-toggle" id="filterButton" onclick="recipeFilterManager.toggleFilterPanel()" title="Filter recipes">
|
|
<i class="fas fa-filter"></i>
|
|
<span class="filter-badge" id="activeFiltersCount" style="display: none">0</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recipe filter panel -->
|
|
<div id="filterPanel" class="filter-panel hidden">
|
|
<div class="filter-header">
|
|
<h3>Filter Recipes</h3>
|
|
<button class="close-filter-btn" onclick="recipeFilterManager.closeFilterPanel()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="filter-section">
|
|
<h4>Base Model</h4>
|
|
<div class="filter-tags" id="baseModelTags">
|
|
<!-- Tags will be dynamically inserted here -->
|
|
</div>
|
|
</div>
|
|
<div class="filter-section">
|
|
<h4>LoRAs</h4>
|
|
<div class="filter-tags" id="loraModelTags">
|
|
<!-- LoRA tags will be dynamically inserted here -->
|
|
</div>
|
|
</div>
|
|
<div class="filter-actions">
|
|
<button class="clear-filters-btn" onclick="recipeFilterManager.clearFilters()">
|
|
Clear All Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recipe gallery container -->
|
|
<div class="recipe-grid" id="recipeGrid">
|
|
{% if recipes and recipes|length > 0 %}
|
|
{% for recipe in recipes %}
|
|
<div class="recipe-card" data-file-path="{{ recipe.file_path }}" data-title="{{ recipe.title }}" data-created="{{ recipe.created_date }}">
|
|
<div class="recipe-indicator" title="Recipe">R</div>
|
|
<div class="card-preview">
|
|
<img src="{{ recipe.file_url }}" alt="{{ recipe.title }}">
|
|
<div class="card-header">
|
|
<div class="base-model-wrapper">
|
|
{% if recipe.base_model %}
|
|
<span class="base-model-label" title="{{ recipe.base_model }}">
|
|
{{ recipe.base_model }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-actions">
|
|
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
|
<i class="fas fa-copy" title="Copy Recipe"></i>
|
|
<i class="fas fa-trash" title="Delete Recipe"></i>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div class="model-info">
|
|
<span class="model-name">{{ recipe.title }}</span>
|
|
</div>
|
|
<div class="lora-count" title="Number of LoRAs in this recipe">
|
|
<i class="fas fa-layer-group"></i> {{ recipe.loras|length }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="placeholder-message">
|
|
<p>No recipes found</p>
|
|
<p>Add recipe images to your recipes folder to see them here.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script type="module" src="/loras_static/js/recipes.js"></script>
|
|
{% if is_initializing %}
|
|
<script>
|
|
// Check initialization status and set auto-refresh
|
|
async function checkInitStatus() {
|
|
try {
|
|
const response = await fetch('/api/recipes?page=1&page_size=1');
|
|
if (response.ok) {
|
|
// If data successfully retrieved, initialization is complete, refresh the page
|
|
window.location.reload();
|
|
} else {
|
|
// If not yet complete, continue polling
|
|
setTimeout(checkInitStatus, 2000); // Check every 2 seconds
|
|
}
|
|
} catch (error) {
|
|
// If error, continue polling
|
|
setTimeout(checkInitStatus, 2000);
|
|
}
|
|
}
|
|
|
|
// Start status checking
|
|
checkInitStatus();
|
|
</script>
|
|
{% endif %}
|
|
|
|
<!-- Recipe page specific scripts -->
|
|
<script>
|
|
// Toggle recipe tags container
|
|
function toggleTagsContainer() {
|
|
const container = document.querySelector('.recipe-tag-container');
|
|
const button = document.querySelector('.toggle-folders-btn');
|
|
|
|
if (container.style.display === 'none') {
|
|
container.style.display = 'flex';
|
|
button.querySelector('i').className = 'fas fa-chevron-up';
|
|
button.title = 'Collapse tags';
|
|
} else {
|
|
container.style.display = 'none';
|
|
button.querySelector('i').className = 'fas fa-chevron-down';
|
|
button.title = 'Expand tags';
|
|
}
|
|
}
|
|
|
|
// Refresh recipes
|
|
function refreshRecipes() {
|
|
// Will be implemented in recipes.js
|
|
window.location.reload();
|
|
}
|
|
|
|
// Simple utility function to handle theme toggling
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-theme');
|
|
localStorage.setItem('theme', document.body.classList.contains('dark-theme') ? 'dark' : 'light');
|
|
}
|
|
|
|
// Apply saved theme on load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme === 'dark') {
|
|
document.body.classList.add('dark-theme');
|
|
}
|
|
|
|
// Setup recipe tag filtering
|
|
document.querySelectorAll('.recipe-tag').forEach(tag => {
|
|
tag.addEventListener('click', () => {
|
|
document.querySelectorAll('.recipe-tag').forEach(t => t.classList.remove('active'));
|
|
tag.classList.add('active');
|
|
|
|
// Implement filtering logic here or in recipes.js
|
|
console.log('Filter by tag:', tag.dataset.tag);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Placeholder for recipe filter manager
|
|
const recipeFilterManager = {
|
|
toggleFilterPanel() {
|
|
const panel = document.getElementById('filterPanel');
|
|
panel.classList.toggle('hidden');
|
|
},
|
|
|
|
closeFilterPanel() {
|
|
document.getElementById('filterPanel').classList.add('hidden');
|
|
},
|
|
|
|
clearFilters() {
|
|
// Clear filters and reset UI
|
|
document.querySelectorAll('.filter-tags .tag.active').forEach(tag => {
|
|
tag.classList.remove('active');
|
|
});
|
|
|
|
document.getElementById('activeFiltersCount').style.display = 'none';
|
|
|
|
// Reapply default view
|
|
refreshRecipes();
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html> |