mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
checkpoint
This commit is contained in:
475
static/css/components/import-modal.css
Normal file
475
static/css/components/import-modal.css
Normal file
@@ -0,0 +1,475 @@
|
||||
/* Import Modal Styles */
|
||||
.import-step {
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.input-group input,
|
||||
.input-group select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--lora-error);
|
||||
font-size: 0.9em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Image Upload Styles */
|
||||
.image-upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-preview .placeholder {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type="file"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input-wrapper .file-input-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-input-wrapper:hover .file-input-button {
|
||||
background: var(--lora-surface-hover);
|
||||
}
|
||||
|
||||
/* Recipe Details Styles */
|
||||
.recipe-details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.recipe-name-container {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.recipe-name-container label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recipe-name-container input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tags-section {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.tags-section label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag-input-container input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.recipe-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.recipe-tag i {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.recipe-tag i:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-tags {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* LoRAs List Styles */
|
||||
.loras-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: var(--space-2) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.lora-item {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--bg-color);
|
||||
margin: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lora-item.exists-locally {
|
||||
background: oklch(var(--lora-accent) / 0.05);
|
||||
border-left: 4px solid var(--lora-accent);
|
||||
}
|
||||
|
||||
.lora-item.missing-locally {
|
||||
background: oklch(var(--lora-error) / 0.05);
|
||||
border-left: 4px solid var(--lora-error);
|
||||
}
|
||||
|
||||
.lora-thumbnail {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--border-radius-xs);
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.lora-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lora-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lora-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.lora-content h3 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
color: var(--text-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lora-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.lora-info .base-model {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
color: var(--lora-accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.weight-badge {
|
||||
background: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.lora-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.85em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.lora-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.local-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.local-badge i {
|
||||
margin-right: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.missing-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--lora-error);
|
||||
color: var(--lora-text);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.missing-badge i {
|
||||
margin-right: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.local-path {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: var(--space-1);
|
||||
margin-top: 4px;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.local-badge:hover .local-path {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Missing LoRAs List */
|
||||
.missing-loras-list {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin: var(--space-2) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.missing-lora-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: oklch(var(--lora-error) / 0.05);
|
||||
border-left: 4px solid var(--lora-error);
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.lora-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lora-type {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Folder Browser Styles */
|
||||
.folder-browser {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: var(--space-1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Modal Header Styles - Updated to match download-modal */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* align-items: center; */
|
||||
margin-bottom: var(--space-3);
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 0 5px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Recipe Details Layout */
|
||||
.recipe-details-layout {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.recipe-image-container {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
.recipe-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.recipe-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.recipe-form-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* Simplify file input for step 1 */
|
||||
.file-input-wrapper {
|
||||
margin: var(--space-3) auto;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* Update LoRA item styles to include version */
|
||||
.lora-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lora-version {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.lora-count-info {
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
margin-left: 8px;
|
||||
}
|
||||
565
static/js/managers/ImportManager.js
Normal file
565
static/js/managers/ImportManager.js
Normal file
@@ -0,0 +1,565 @@
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { LoadingManager } from './LoadingManager.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { resetAndReload } from '../api/loraApi.js';
|
||||
|
||||
export class ImportManager {
|
||||
constructor() {
|
||||
this.recipeImage = null;
|
||||
this.recipeData = null;
|
||||
this.recipeName = '';
|
||||
this.recipeTags = [];
|
||||
this.missingLoras = [];
|
||||
|
||||
// Add initialization check
|
||||
this.initialized = false;
|
||||
this.selectedFolder = '';
|
||||
|
||||
// Add LoadingManager instance
|
||||
this.loadingManager = new LoadingManager();
|
||||
this.folderClickHandler = null;
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
}
|
||||
|
||||
showImportModal() {
|
||||
console.log('Showing import modal...');
|
||||
if (!this.initialized) {
|
||||
// Check if modal exists
|
||||
const modal = document.getElementById('importModal');
|
||||
if (!modal) {
|
||||
console.error('Import modal element not found');
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
modalManager.showModal('importModal', null, () => {
|
||||
// Cleanup handler when modal closes
|
||||
this.cleanupFolderBrowser();
|
||||
});
|
||||
this.resetSteps();
|
||||
}
|
||||
|
||||
resetSteps() {
|
||||
document.querySelectorAll('.import-step').forEach(step => step.style.display = 'none');
|
||||
document.getElementById('uploadStep').style.display = 'block';
|
||||
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('recipeImageUpload');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
// Reset error message
|
||||
const errorElement = document.getElementById('uploadError');
|
||||
if (errorElement) {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
|
||||
// Reset preview
|
||||
const previewElement = document.getElementById('imagePreview');
|
||||
if (previewElement) {
|
||||
previewElement.innerHTML = '<div class="placeholder">Image preview will appear here</div>';
|
||||
}
|
||||
|
||||
// Reset state variables
|
||||
this.recipeImage = null;
|
||||
this.recipeData = null;
|
||||
this.recipeName = '';
|
||||
this.recipeTags = [];
|
||||
this.missingLoras = [];
|
||||
|
||||
// Clear selected folder and remove selection from UI
|
||||
this.selectedFolder = '';
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (folderBrowser) {
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
}
|
||||
}
|
||||
|
||||
handleImageUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
const errorElement = document.getElementById('uploadError');
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.match('image.*')) {
|
||||
errorElement.textContent = 'Please select an image file';
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset error
|
||||
errorElement.textContent = '';
|
||||
this.recipeImage = file;
|
||||
|
||||
// Auto-proceed to next step if file is selected
|
||||
this.uploadAndAnalyzeImage();
|
||||
}
|
||||
|
||||
async uploadAndAnalyzeImage() {
|
||||
if (!this.recipeImage) {
|
||||
showToast('Please select an image first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loadingManager.showSimpleLoading('Analyzing image metadata...');
|
||||
|
||||
// Create form data for upload
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.recipeImage);
|
||||
|
||||
// Upload image for analysis
|
||||
const response = await fetch('/api/recipes/analyze-image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
// Get recipe data from response
|
||||
this.recipeData = await response.json();
|
||||
|
||||
// Check if we have an error message
|
||||
if (this.recipeData.error) {
|
||||
throw new Error(this.recipeData.error);
|
||||
}
|
||||
|
||||
// Check if we have valid recipe data
|
||||
if (!this.recipeData || !this.recipeData.loras || this.recipeData.loras.length === 0) {
|
||||
throw new Error('No LoRA information found in this image');
|
||||
}
|
||||
|
||||
// Find missing LoRAs
|
||||
this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally);
|
||||
|
||||
// Proceed to recipe details step
|
||||
this.showRecipeDetailsStep();
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('uploadError').textContent = error.message;
|
||||
} finally {
|
||||
this.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
showRecipeDetailsStep() {
|
||||
document.getElementById('uploadStep').style.display = 'none';
|
||||
document.getElementById('detailsStep').style.display = 'block';
|
||||
|
||||
// Set default recipe name from image filename
|
||||
const recipeName = document.getElementById('recipeName');
|
||||
if (this.recipeImage && !recipeName.value) {
|
||||
const fileName = this.recipeImage.name.split('.')[0];
|
||||
recipeName.value = fileName;
|
||||
this.recipeName = fileName;
|
||||
}
|
||||
|
||||
// Display the uploaded image in the preview
|
||||
const imagePreview = document.getElementById('recipeImagePreview');
|
||||
if (imagePreview && this.recipeImage) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
imagePreview.innerHTML = `<img src="${e.target.result}" alt="Recipe preview">`;
|
||||
};
|
||||
reader.readAsDataURL(this.recipeImage);
|
||||
}
|
||||
|
||||
// Update LoRA count information
|
||||
const totalLoras = this.recipeData.loras.length;
|
||||
const existingLoras = this.recipeData.loras.filter(lora => lora.existsLocally).length;
|
||||
const loraCountInfo = document.getElementById('loraCountInfo');
|
||||
if (loraCountInfo) {
|
||||
loraCountInfo.textContent = `(${existingLoras}/${totalLoras} in library)`;
|
||||
}
|
||||
|
||||
// Display LoRAs list
|
||||
const lorasList = document.getElementById('lorasList');
|
||||
if (lorasList) {
|
||||
lorasList.innerHTML = this.recipeData.loras.map(lora => {
|
||||
const existsLocally = lora.existsLocally;
|
||||
const localPath = lora.localPath || '';
|
||||
|
||||
// Create local status badge
|
||||
const localStatus = existsLocally ?
|
||||
`<div class="local-badge">
|
||||
<i class="fas fa-check"></i> In Library
|
||||
<div class="local-path">${localPath}</div>
|
||||
</div>` :
|
||||
`<div class="missing-badge">
|
||||
<i class="fas fa-exclamation-triangle"></i> Not in Library
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<div class="lora-item ${existsLocally ? 'exists-locally' : 'missing-locally'}">
|
||||
<div class="lora-thumbnail">
|
||||
<img src="${lora.thumbnailUrl || '/loras_static/images/no-preview.png'}" alt="LoRA preview">
|
||||
</div>
|
||||
<div class="lora-content">
|
||||
<div class="lora-header">
|
||||
<h3>${lora.name}</h3>
|
||||
${localStatus}
|
||||
</div>
|
||||
${lora.version ? `<div class="lora-version">${lora.version}</div>` : ''}
|
||||
<div class="lora-info">
|
||||
${lora.baseModel ? `<div class="base-model">${lora.baseModel}</div>` : ''}
|
||||
<div class="weight-badge">Weight: ${lora.weight || 1.0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update Next button state based on missing LoRAs
|
||||
this.updateNextButtonState();
|
||||
}
|
||||
|
||||
updateNextButtonState() {
|
||||
const nextButton = document.querySelector('#detailsStep .primary-btn');
|
||||
if (!nextButton) return;
|
||||
|
||||
// If we have missing LoRAs, show "Download Missing LoRAs"
|
||||
// Otherwise show "Save Recipe"
|
||||
if (this.missingLoras.length > 0) {
|
||||
nextButton.textContent = 'Download Missing LoRAs';
|
||||
} else {
|
||||
nextButton.textContent = 'Save Recipe';
|
||||
}
|
||||
}
|
||||
|
||||
handleRecipeNameChange(event) {
|
||||
this.recipeName = event.target.value.trim();
|
||||
}
|
||||
|
||||
addTag() {
|
||||
const tagInput = document.getElementById('tagInput');
|
||||
const tag = tagInput.value.trim();
|
||||
|
||||
if (!tag) return;
|
||||
|
||||
if (!this.recipeTags.includes(tag)) {
|
||||
this.recipeTags.push(tag);
|
||||
this.updateTagsDisplay();
|
||||
}
|
||||
|
||||
tagInput.value = '';
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
this.recipeTags = this.recipeTags.filter(t => t !== tag);
|
||||
this.updateTagsDisplay();
|
||||
}
|
||||
|
||||
updateTagsDisplay() {
|
||||
const tagsContainer = document.getElementById('tagsContainer');
|
||||
|
||||
if (this.recipeTags.length === 0) {
|
||||
tagsContainer.innerHTML = '<div class="empty-tags">No tags added</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tagsContainer.innerHTML = this.recipeTags.map(tag => `
|
||||
<div class="recipe-tag">
|
||||
${tag}
|
||||
<i class="fas fa-times" onclick="importManager.removeTag('${tag}')"></i>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
proceedFromDetails() {
|
||||
// Validate recipe name
|
||||
if (!this.recipeName) {
|
||||
showToast('Please enter a recipe name', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have missing LoRAs, go to location step
|
||||
if (this.missingLoras.length > 0) {
|
||||
this.proceedToLocation();
|
||||
} else {
|
||||
// Otherwise, save the recipe directly
|
||||
this.saveRecipe();
|
||||
}
|
||||
}
|
||||
|
||||
async proceedToLocation() {
|
||||
document.getElementById('detailsStep').style.display = 'none';
|
||||
document.getElementById('locationStep').style.display = 'block';
|
||||
|
||||
try {
|
||||
this.loadingManager.showSimpleLoading('Loading download options...');
|
||||
|
||||
const response = await fetch('/api/lora-roots');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch LoRA roots');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
|
||||
// Check if we have roots
|
||||
if (!data.roots || data.roots.length === 0) {
|
||||
throw new Error('No LoRA root directories configured');
|
||||
}
|
||||
|
||||
// Populate roots dropdown
|
||||
loraRoot.innerHTML = data.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
// Initialize folder browser after loading roots
|
||||
await this.initializeFolderBrowser();
|
||||
|
||||
// Display missing LoRAs
|
||||
const missingLorasList = document.getElementById('missingLorasList');
|
||||
if (missingLorasList) {
|
||||
missingLorasList.innerHTML = this.missingLoras.map(lora => `
|
||||
<div class="missing-lora-item">
|
||||
<div class="lora-name">${lora.name}</div>
|
||||
<div class="lora-type">${lora.type || 'lora'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Update target path display
|
||||
this.updateTargetPath();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in proceedToLocation:', error);
|
||||
showToast(error.message, 'error');
|
||||
// Go back to details step on error
|
||||
this.backToDetails();
|
||||
} finally {
|
||||
this.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
backToUpload() {
|
||||
document.getElementById('detailsStep').style.display = 'none';
|
||||
document.getElementById('uploadStep').style.display = 'block';
|
||||
}
|
||||
|
||||
backToDetails() {
|
||||
document.getElementById('locationStep').style.display = 'none';
|
||||
document.getElementById('detailsStep').style.display = 'block';
|
||||
}
|
||||
|
||||
async saveRecipe() {
|
||||
try {
|
||||
// If we're in the location step, we need to download missing LoRAs first
|
||||
if (document.getElementById('locationStep').style.display !== 'none') {
|
||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
||||
const newFolder = document.getElementById('importNewFolder').value.trim();
|
||||
|
||||
if (!loraRoot) {
|
||||
showToast('Please select a LoRA root directory', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct relative path
|
||||
let targetFolder = '';
|
||||
if (this.selectedFolder) {
|
||||
targetFolder = this.selectedFolder;
|
||||
}
|
||||
if (newFolder) {
|
||||
targetFolder = targetFolder ?
|
||||
`${targetFolder}/${newFolder}` : newFolder;
|
||||
}
|
||||
|
||||
// Show loading with progress bar for download
|
||||
this.loadingManager.show('Downloading missing LoRAs...', 0);
|
||||
|
||||
// Setup WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.status === 'progress') {
|
||||
this.loadingManager.setProgress(data.progress);
|
||||
this.loadingManager.setStatus(`Downloading: ${data.progress}%`);
|
||||
}
|
||||
};
|
||||
|
||||
// Download missing LoRAs
|
||||
const downloadResponse = await fetch('/api/recipes/download-missing-loras', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
loras: this.missingLoras,
|
||||
lora_root: loraRoot,
|
||||
relative_path: targetFolder
|
||||
})
|
||||
});
|
||||
|
||||
if (!downloadResponse.ok) {
|
||||
throw new Error(await downloadResponse.text());
|
||||
}
|
||||
|
||||
// Update missing LoRAs with downloaded paths
|
||||
const downloadResult = await downloadResponse.json();
|
||||
this.recipeData.loras = this.recipeData.loras.map(lora => {
|
||||
const downloaded = downloadResult.downloaded.find(d => d.id === lora.id);
|
||||
if (downloaded) {
|
||||
return {
|
||||
...lora,
|
||||
existsLocally: true,
|
||||
localPath: downloaded.localPath
|
||||
};
|
||||
}
|
||||
return lora;
|
||||
});
|
||||
}
|
||||
|
||||
// Now save the recipe
|
||||
this.loadingManager.showSimpleLoading('Saving recipe...');
|
||||
|
||||
// Create form data for recipe save
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.recipeImage);
|
||||
formData.append('name', this.recipeName);
|
||||
formData.append('tags', JSON.stringify(this.recipeTags));
|
||||
formData.append('recipe_data', JSON.stringify(this.recipeData));
|
||||
|
||||
// Save recipe
|
||||
const saveResponse = await fetch('/api/recipes/save', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error(await saveResponse.text());
|
||||
}
|
||||
|
||||
showToast('Recipe saved successfully', 'success');
|
||||
modalManager.closeModal('importModal');
|
||||
|
||||
// Reload recipes
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
} finally {
|
||||
this.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Add new method to handle folder selection
|
||||
async initializeFolderBrowser() {
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (!folderBrowser) return;
|
||||
|
||||
// Cleanup existing handler if any
|
||||
this.cleanupFolderBrowser();
|
||||
|
||||
try {
|
||||
// Get the selected root
|
||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
||||
if (!loraRoot) {
|
||||
folderBrowser.innerHTML = '<div class="empty-folder">Please select a LoRA root directory</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch folders for the selected root
|
||||
const response = await fetch(`/api/folders?root=${encodeURIComponent(loraRoot)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch folders');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Display folders
|
||||
if (data.folders && data.folders.length > 0) {
|
||||
folderBrowser.innerHTML = data.folders.map(folder => `
|
||||
<div class="folder-item" data-folder="${folder}">
|
||||
<i class="fas fa-folder"></i> ${folder}
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
folderBrowser.innerHTML = '<div class="empty-folder">No folders found</div>';
|
||||
}
|
||||
|
||||
// Create new handler
|
||||
this.folderClickHandler = (event) => {
|
||||
const folderItem = event.target.closest('.folder-item');
|
||||
if (!folderItem) return;
|
||||
|
||||
if (folderItem.classList.contains('selected')) {
|
||||
folderItem.classList.remove('selected');
|
||||
this.selectedFolder = '';
|
||||
} else {
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
folderItem.classList.add('selected');
|
||||
this.selectedFolder = folderItem.dataset.folder;
|
||||
}
|
||||
|
||||
// Update path display after folder selection
|
||||
this.updateTargetPath();
|
||||
};
|
||||
|
||||
// Add the new handler
|
||||
folderBrowser.addEventListener('click', this.folderClickHandler);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing folder browser:', error);
|
||||
folderBrowser.innerHTML = `<div class="error-message">Error: ${error.message}</div>`;
|
||||
}
|
||||
|
||||
// Add event listeners for path updates
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
const newFolder = document.getElementById('importNewFolder');
|
||||
|
||||
loraRoot.addEventListener('change', async () => {
|
||||
await this.initializeFolderBrowser();
|
||||
this.updateTargetPath();
|
||||
});
|
||||
|
||||
newFolder.addEventListener('input', this.updateTargetPath);
|
||||
|
||||
// Update initial path
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
cleanupFolderBrowser() {
|
||||
if (this.folderClickHandler) {
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (folderBrowser) {
|
||||
folderBrowser.removeEventListener('click', this.folderClickHandler);
|
||||
this.folderClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove path update listeners
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
const newFolder = document.getElementById('importNewFolder');
|
||||
|
||||
if (loraRoot) loraRoot.removeEventListener('change', this.updateTargetPath);
|
||||
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
|
||||
}
|
||||
|
||||
// Add new method to update target path
|
||||
updateTargetPath() {
|
||||
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
||||
if (!pathDisplay) return;
|
||||
|
||||
const loraRoot = document.getElementById('importLoraRoot')?.value || '';
|
||||
const newFolder = document.getElementById('importNewFolder')?.value.trim() || '';
|
||||
|
||||
let fullPath = loraRoot || 'Select a LoRA root directory';
|
||||
|
||||
if (loraRoot) {
|
||||
if (this.selectedFolder) {
|
||||
fullPath += '/' + this.selectedFolder;
|
||||
}
|
||||
if (newFolder) {
|
||||
fullPath += '/' + newFolder;
|
||||
}
|
||||
}
|
||||
|
||||
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,15 @@ export class ModalManager {
|
||||
}
|
||||
});
|
||||
|
||||
// Add importModal registration
|
||||
this.registerModal('importModal', {
|
||||
element: document.getElementById('importModal'),
|
||||
onClose: () => {
|
||||
this.getModal('importModal').element.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
});
|
||||
|
||||
// Set up event listeners for modal toggles
|
||||
const supportToggle = document.getElementById('supportToggleBtn');
|
||||
if (supportToggle) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { showToast } from './utils/uiHelpers.js';
|
||||
import { state } from './state/index.js';
|
||||
import { initializeCommonComponents } from './common.js';
|
||||
import { ImportManager } from './managers/ImportManager.js';
|
||||
|
||||
class RecipeManager {
|
||||
constructor() {
|
||||
@@ -10,6 +11,9 @@ class RecipeManager {
|
||||
this.sortBy = 'date';
|
||||
this.filterParams = {};
|
||||
|
||||
// Initialize ImportManager
|
||||
this.importManager = new ImportManager();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -43,6 +47,15 @@ class RecipeManager {
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Import button
|
||||
const importButton = document.querySelector('button[onclick="importRecipes()"]');
|
||||
if (importButton) {
|
||||
importButton.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
this.importManager.showImportModal();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async loadRecipes() {
|
||||
@@ -198,12 +211,25 @@ class RecipeManager {
|
||||
// - Recipe details view
|
||||
// - Recipe tag filtering
|
||||
// - Recipe search and filters
|
||||
|
||||
// Add a method to handle recipe import
|
||||
importRecipes() {
|
||||
this.importManager.showImportModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeCommonComponents();
|
||||
window.recipeManager = new RecipeManager();
|
||||
|
||||
// Make importRecipes function available globally
|
||||
window.importRecipes = () => {
|
||||
window.recipeManager.importRecipes();
|
||||
};
|
||||
|
||||
// Expose ImportManager instance globally for the import modal event handlers
|
||||
window.importManager = window.recipeManager.importManager;
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
|
||||
Reference in New Issue
Block a user