diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index ae39a141..bec00d73 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -2,34 +2,41 @@ import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; import { LoadingManager } from './LoadingManager.js'; import { getStorageItem } from '../utils/storageHelpers.js'; +import { ImportStepManager } from './import/ImportStepManager.js'; +import { ImageProcessor } from './import/ImageProcessor.js'; +import { RecipeDataManager } from './import/RecipeDataManager.js'; +import { DownloadManager } from './import/DownloadManager.js'; +import { FolderBrowser } from './import/FolderBrowser.js'; +import { formatFileSize } from '../utils/formatters.js'; export class ImportManager { constructor() { + // Core state properties this.recipeImage = null; this.recipeData = null; this.recipeName = ''; this.recipeTags = []; this.missingLoras = []; - - // Add initialization check this.initialized = false; this.selectedFolder = ''; - - // Add LoadingManager instance + this.downloadableLoRAs = []; + this.recipeId = null; + this.importMode = 'url'; // Default mode: 'url' or 'upload' + + // Initialize sub-managers this.loadingManager = new LoadingManager(); - this.folderClickHandler = null; - this.updateTargetPath = this.updateTargetPath.bind(this); + this.stepManager = new ImportStepManager(); + this.imageProcessor = new ImageProcessor(this); + this.recipeDataManager = new RecipeDataManager(this); + this.downloadManager = new DownloadManager(this); + this.folderBrowser = new FolderBrowser(this); - // 添加对注入样式的引用 - this.injectedStyles = null; - - // Change default mode to url/path - this.importMode = 'url'; // Default mode changed to: 'url' or 'upload' + // Bind methods + this.formatFileSize = formatFileSize; } showImportModal(recipeData = null, recipeId = null) { if (!this.initialized) { - // Check if modal exists const modal = document.getElementById('importModal'); if (!modal) { console.error('Import modal element not found'); @@ -38,82 +45,46 @@ export class ImportManager { this.initialized = true; } - // Always reset the state when opening the modal + // Reset state this.resetSteps(); if (recipeData) { this.downloadableLoRAs = recipeData.loras; this.recipeId = recipeId; } - // Show the modal + // Show modal modalManager.showModal('importModal', null, () => { - // Cleanup handler when modal closes - this.cleanupFolderBrowser(); - - // Remove any injected styles - this.removeInjectedStyles(); + this.folderBrowser.cleanup(); + this.stepManager.removeInjectedStyles(); }); - // Verify the modal is properly shown - setTimeout(() => { - this.ensureModalVisible(); - }, 50); - } - - // 添加移除注入样式的方法 - removeInjectedStyles() { - if (this.injectedStyles && this.injectedStyles.parentNode) { - this.injectedStyles.parentNode.removeChild(this.injectedStyles); - this.injectedStyles = null; - } - - // Also reset any inline styles that might have been set with !important - document.querySelectorAll('.import-step').forEach(step => { - step.style.cssText = ''; - }); + // Verify visibility + setTimeout(() => this.ensureModalVisible(), 50); } resetSteps() { - // Remove any existing injected styles - this.removeInjectedStyles(); + // Clear UI state + this.stepManager.removeInjectedStyles(); + this.stepManager.showStep('uploadStep'); - // Show the first step - this.showStep('uploadStep'); - - // Reset file input + // Reset form inputs const fileInput = document.getElementById('recipeImageUpload'); - if (fileInput) { - fileInput.value = ''; - } + if (fileInput) fileInput.value = ''; - // Reset URL input const urlInput = document.getElementById('imageUrlInput'); - if (urlInput) { - urlInput.value = ''; - } + if (urlInput) urlInput.value = ''; - // Reset error messages const uploadError = document.getElementById('uploadError'); - if (uploadError) { - uploadError.textContent = ''; - } + if (uploadError) uploadError.textContent = ''; const urlError = document.getElementById('urlError'); - if (urlError) { - urlError.textContent = ''; - } + if (urlError) urlError.textContent = ''; - // Reset recipe name input const recipeName = document.getElementById('recipeName'); - if (recipeName) { - recipeName.value = ''; - } + if (recipeName) recipeName.value = ''; - // Reset tags container const tagsContainer = document.getElementById('tagsContainer'); - if (tagsContainer) { - tagsContainer.innerHTML = '
'; - } + if (tagsContainer) tagsContainer.innerHTML = ''; // Reset state variables this.recipeImage = null; @@ -123,11 +94,11 @@ export class ImportManager { this.missingLoras = []; this.downloadableLoRAs = []; - // Reset import mode to url/path instead of upload + // Reset import mode this.importMode = 'url'; this.toggleImportMode('url'); - // Clear selected folder and remove selection from UI + // Reset folder browser this.selectedFolder = ''; const folderBrowser = document.getElementById('importFolderBrowser'); if (folderBrowser) { @@ -135,29 +106,20 @@ export class ImportManager { f.classList.remove('selected')); } - // Clear missing LoRAs list if it exists + // Clear missing LoRAs list const missingLorasList = document.getElementById('missingLorasList'); - if (missingLorasList) { - missingLorasList.innerHTML = ''; - } + if (missingLorasList) missingLorasList.innerHTML = ''; // Reset total download size const totalSizeDisplay = document.getElementById('totalDownloadSize'); - if (totalSizeDisplay) { - totalSizeDisplay.textContent = 'Calculating...'; - } + if (totalSizeDisplay) totalSizeDisplay.textContent = 'Calculating...'; - // Remove any existing deleted LoRAs warning + // Remove warnings const deletedLorasWarning = document.getElementById('deletedLorasWarning'); - if (deletedLorasWarning) { - deletedLorasWarning.remove(); - } + if (deletedLorasWarning) deletedLorasWarning.remove(); - // Remove any existing early access warning const earlyAccessWarning = document.getElementById('earlyAccessWarning'); - if (earlyAccessWarning) { - earlyAccessWarning.remove(); - } + if (earlyAccessWarning) earlyAccessWarning.remove(); } toggleImportMode(mode) { @@ -200,433 +162,19 @@ export class ImportManager { } 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(); + this.imageProcessor.handleFileUpload(event); } async handleUrlInput() { - const urlInput = document.getElementById('imageUrlInput'); - const errorElement = document.getElementById('urlError'); - const input = urlInput.value.trim(); - - // Validate input - if (!input) { - errorElement.textContent = 'Please enter a URL or file path'; - return; - } - - // Reset error - errorElement.textContent = ''; - - // Show loading indicator - this.loadingManager.showSimpleLoading('Processing input...'); - - try { - // Check if it's a URL or a local file path - if (input.startsWith('http://') || input.startsWith('https://')) { - // Handle as URL - await this.analyzeImageFromUrl(input); - } else { - // Handle as local file path - await this.analyzeImageFromLocalPath(input); - } - } catch (error) { - errorElement.textContent = error.message || 'Failed to process input'; - } finally { - this.loadingManager.hide(); - } - } - - async analyzeImageFromUrl(url) { - try { - // Call the API with URL data - const response = await fetch('/api/recipes/analyze-image', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ url: url }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to analyze image from URL'); - } - - // 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) { - console.error('Error analyzing URL:', error); - throw error; - } - } - - // Add new method to handle local file paths - async analyzeImageFromLocalPath(path) { - try { - // Call the API with local path data - const response = await fetch('/api/recipes/analyze-local-image', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ path: path }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to load image from local path'); - } - - // 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) { - console.error('Error analyzing local path:', error); - throw error; - } + await this.imageProcessor.handleUrlInput(); } 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(); - - console.log('Recipe data:', this.recipeData); - - // 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'); - } - - // Store generation parameters if available - if (this.recipeData.gen_params) { - console.log('Generation parameters found:', this.recipeData.gen_params); - } - - // 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(); - } + await this.imageProcessor.uploadAndAnalyzeImage(); } showRecipeDetailsStep() { - this.showStep('detailsStep'); - - // Set default recipe name from prompt or image filename - const recipeName = document.getElementById('recipeName'); - - // Check if we have recipe metadata from a shared recipe - if (this.recipeData && this.recipeData.from_recipe_metadata) { - // Use title from recipe metadata - if (this.recipeData.title) { - recipeName.value = this.recipeData.title; - this.recipeName = this.recipeData.title; - } - - // Use tags from recipe metadata - if (this.recipeData.tags && Array.isArray(this.recipeData.tags)) { - this.recipeTags = [...this.recipeData.tags]; - this.updateTagsDisplay(); - } - } else if (this.recipeData && this.recipeData.gen_params && this.recipeData.gen_params.prompt) { - // Use the first 10 words from the prompt as the default recipe name - const promptWords = this.recipeData.gen_params.prompt.split(' '); - const truncatedPrompt = promptWords.slice(0, 10).join(' '); - recipeName.value = truncatedPrompt; - this.recipeName = truncatedPrompt; - - // Set up click handler to select all text for easy editing - if (!recipeName.hasSelectAllHandler) { - recipeName.addEventListener('click', function() { - this.select(); - }); - recipeName.hasSelectAllHandler = true; - } - } else if (this.recipeImage && !recipeName.value) { - // Fallback to image filename if no prompt is available - const fileName = this.recipeImage.name.split('.')[0]; - recipeName.value = fileName; - this.recipeName = fileName; - } - - // Always set up click handler for easy editing if not already set - if (!recipeName.hasSelectAllHandler) { - recipeName.addEventListener('click', function() { - this.select(); - }); - recipeName.hasSelectAllHandler = true; - } - - // Display the uploaded image in the preview - const imagePreview = document.getElementById('recipeImagePreview'); - if (imagePreview) { - if (this.recipeImage) { - // For file upload mode - const reader = new FileReader(); - reader.onload = (e) => { - imagePreview.innerHTML = `