mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
refactor: Clean up and optimize import modal and related components, removing unused styles and improving path selection functionality
This commit is contained in:
@@ -628,15 +628,6 @@ class ModelRouteUtils:
|
||||
if not result.get('success', False):
|
||||
error_message = result.get('error', 'Unknown error')
|
||||
|
||||
# Return 401 for early access errors
|
||||
if 'early access' in error_message.lower():
|
||||
logger.warning(f"Early access download failed: {error_message}")
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': f"Early Access Restriction: {error_message}",
|
||||
'download_id': download_id
|
||||
}, status=401)
|
||||
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': error_message,
|
||||
|
||||
@@ -337,72 +337,7 @@
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Location Selection Styles */
|
||||
.location-selection {
|
||||
margin: var(--space-2) 0;
|
||||
padding: var(--space-2);
|
||||
background: var(--lora-surface);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
/* Reuse folder browser and path preview styles from download-modal.css */
|
||||
.folder-browser {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: var(--space-1);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.folder-item:hover {
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.folder-item.selected {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
border: 1px solid var(--lora-accent);
|
||||
}
|
||||
|
||||
.path-preview {
|
||||
margin-bottom: var(--space-3);
|
||||
padding: var(--space-2);
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
border: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.path-preview label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.path-display {
|
||||
padding: var(--space-1);
|
||||
color: var(--text-color);
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
opacity: 0.85;
|
||||
background: var(--lora-surface);
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
/* Input Group Styles */
|
||||
.input-group {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
@@ -430,22 +365,6 @@
|
||||
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Dark theme adjustments */
|
||||
[data-theme="dark"] .lora-item {
|
||||
background: var(--lora-surface);
|
||||
|
||||
@@ -23,7 +23,7 @@ body.modal-open {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
height: auto;
|
||||
max-height: calc(90vh - 48px); /* Adjust to account for header height */
|
||||
/* max-height: calc(90vh - 48px); */
|
||||
margin: 1rem auto; /* Keep reduced top margin */
|
||||
background: var(--lora-surface);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
@@ -121,15 +121,6 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -4,8 +4,13 @@ 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 { FolderTreeManager } from '../components/FolderTreeManager.js';
|
||||
import { formatFileSize } from '../utils/formatters.js';
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
export class ImportManager {
|
||||
constructor() {
|
||||
@@ -20,6 +25,8 @@ export class ImportManager {
|
||||
this.downloadableLoRAs = [];
|
||||
this.recipeId = null;
|
||||
this.importMode = 'url'; // Default mode: 'url' or 'upload'
|
||||
this.useDefaultPath = false;
|
||||
this.apiClient = null;
|
||||
|
||||
// Initialize sub-managers
|
||||
this.loadingManager = new LoadingManager();
|
||||
@@ -27,10 +34,12 @@ export class ImportManager {
|
||||
this.imageProcessor = new ImageProcessor(this);
|
||||
this.recipeDataManager = new RecipeDataManager(this);
|
||||
this.downloadManager = new DownloadManager(this);
|
||||
this.folderBrowser = new FolderBrowser(this);
|
||||
this.folderTreeManager = new FolderTreeManager();
|
||||
|
||||
// Bind methods
|
||||
this.formatFileSize = formatFileSize;
|
||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||
this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this);
|
||||
}
|
||||
|
||||
showImportModal(recipeData = null, recipeId = null) {
|
||||
@@ -40,9 +49,13 @@ export class ImportManager {
|
||||
console.error('Import modal element not found');
|
||||
return;
|
||||
}
|
||||
this.initializeEventHandlers();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
// Get API client for LoRAs
|
||||
this.apiClient = getModelApiClient(MODEL_TYPES.LORA);
|
||||
|
||||
// Reset state
|
||||
this.resetSteps();
|
||||
if (recipeData) {
|
||||
@@ -52,14 +65,12 @@ export class ImportManager {
|
||||
|
||||
// Show modal
|
||||
modalManager.showModal('importModal', null, () => {
|
||||
this.folderBrowser.cleanup();
|
||||
this.cleanupFolderBrowser();
|
||||
this.stepManager.removeInjectedStyles();
|
||||
});
|
||||
|
||||
|
||||
// Verify visibility and focus on URL input
|
||||
setTimeout(() => {
|
||||
this.ensureModalVisible();
|
||||
|
||||
setTimeout(() => {
|
||||
// Ensure URL option is selected and focus on the input
|
||||
this.toggleImportMode('url');
|
||||
const urlInput = document.getElementById('imageUrlInput');
|
||||
@@ -69,6 +80,14 @@ export class ImportManager {
|
||||
}, 50);
|
||||
}
|
||||
|
||||
initializeEventHandlers() {
|
||||
// Default path toggle handler
|
||||
const useDefaultPathToggle = document.getElementById('importUseDefaultPath');
|
||||
if (useDefaultPathToggle) {
|
||||
useDefaultPathToggle.addEventListener('change', this.handleToggleDefaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
resetSteps() {
|
||||
// Clear UI state
|
||||
this.stepManager.removeInjectedStyles();
|
||||
@@ -93,6 +112,12 @@ export class ImportManager {
|
||||
const tagsContainer = document.getElementById('tagsContainer');
|
||||
if (tagsContainer) tagsContainer.innerHTML = '<div class="empty-tags">No tags added</div>';
|
||||
|
||||
// Clear folder path input
|
||||
const folderPathInput = document.getElementById('importFolderPath');
|
||||
if (folderPathInput) {
|
||||
folderPathInput.value = '';
|
||||
}
|
||||
|
||||
// Reset state variables
|
||||
this.recipeImage = null;
|
||||
this.recipeData = null;
|
||||
@@ -100,33 +125,19 @@ export class ImportManager {
|
||||
this.recipeTags = [];
|
||||
this.missingLoras = [];
|
||||
this.downloadableLoRAs = [];
|
||||
this.selectedFolder = '';
|
||||
|
||||
// Reset import mode
|
||||
this.importMode = 'url';
|
||||
this.toggleImportMode('url');
|
||||
|
||||
// Reset folder browser
|
||||
this.selectedFolder = '';
|
||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||
if (folderBrowser) {
|
||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||
f.classList.remove('selected'));
|
||||
// Clear folder tree selection
|
||||
if (this.folderTreeManager) {
|
||||
this.folderTreeManager.clearSelection();
|
||||
}
|
||||
|
||||
// Clear missing LoRAs list
|
||||
const missingLorasList = document.getElementById('missingLorasList');
|
||||
if (missingLorasList) missingLorasList.innerHTML = '';
|
||||
|
||||
// Reset total download size
|
||||
const totalSizeDisplay = document.getElementById('totalDownloadSize');
|
||||
if (totalSizeDisplay) totalSizeDisplay.textContent = 'Calculating...';
|
||||
|
||||
// Remove warnings
|
||||
const deletedLorasWarning = document.getElementById('deletedLorasWarning');
|
||||
if (deletedLorasWarning) deletedLorasWarning.remove();
|
||||
|
||||
const earlyAccessWarning = document.getElementById('earlyAccessWarning');
|
||||
if (earlyAccessWarning) earlyAccessWarning.remove();
|
||||
// Reset default path toggle
|
||||
this.loadDefaultPathSetting();
|
||||
|
||||
// Reset duplicate related properties
|
||||
this.duplicateRecipes = [];
|
||||
@@ -204,7 +215,54 @@ export class ImportManager {
|
||||
}
|
||||
|
||||
async proceedToLocation() {
|
||||
await this.folderBrowser.proceedToLocation();
|
||||
this.stepManager.showStep('locationStep');
|
||||
|
||||
try {
|
||||
// Fetch LoRA roots
|
||||
const rootsData = await this.apiClient.fetchModelRoots();
|
||||
const loraRoot = document.getElementById('importLoraRoot');
|
||||
loraRoot.innerHTML = rootsData.roots.map(root =>
|
||||
`<option value="${root}">${root}</option>`
|
||||
).join('');
|
||||
|
||||
// Set default root if available
|
||||
const defaultRootKey = 'default_lora_root';
|
||||
const defaultRoot = getStorageItem('settings', {})[defaultRootKey];
|
||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||
loraRoot.value = defaultRoot;
|
||||
}
|
||||
|
||||
// Set autocomplete="off" on folderPath input
|
||||
const folderPathInput = document.getElementById('importFolderPath');
|
||||
if (folderPathInput) {
|
||||
folderPathInput.setAttribute('autocomplete', 'off');
|
||||
}
|
||||
|
||||
// Setup folder tree manager
|
||||
this.folderTreeManager.init({
|
||||
elementsPrefix: 'import',
|
||||
onPathChange: (path) => {
|
||||
this.selectedFolder = path;
|
||||
this.updateTargetPath();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize folder tree
|
||||
await this.initializeFolderTree();
|
||||
|
||||
// Setup lora root change handler
|
||||
loraRoot.addEventListener('change', async () => {
|
||||
await this.initializeFolderTree();
|
||||
this.updateTargetPath();
|
||||
});
|
||||
|
||||
// Load default path setting for LoRAs
|
||||
this.loadDefaultPathSetting();
|
||||
|
||||
this.updateTargetPath();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
backToUpload() {
|
||||
@@ -234,25 +292,107 @@ export class ImportManager {
|
||||
await this.downloadManager.saveRecipe();
|
||||
}
|
||||
|
||||
updateTargetPath() {
|
||||
this.folderBrowser.updateTargetPath();
|
||||
loadDefaultPathSetting() {
|
||||
const storageKey = 'use_default_path_loras';
|
||||
this.useDefaultPath = getStorageItem(storageKey, false);
|
||||
|
||||
const toggleInput = document.getElementById('importUseDefaultPath');
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = this.useDefaultPath;
|
||||
this.updatePathSelectionUI();
|
||||
}
|
||||
}
|
||||
|
||||
ensureModalVisible() {
|
||||
const importModal = document.getElementById('importModal');
|
||||
if (!importModal) {
|
||||
console.error('Import modal element not found');
|
||||
return false;
|
||||
toggleDefaultPath(event) {
|
||||
this.useDefaultPath = event.target.checked;
|
||||
|
||||
// Save to localStorage for LoRAs
|
||||
const storageKey = 'use_default_path_loras';
|
||||
setStorageItem(storageKey, this.useDefaultPath);
|
||||
|
||||
this.updatePathSelectionUI();
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
updatePathSelectionUI() {
|
||||
const manualSelection = document.getElementById('importManualPathSelection');
|
||||
|
||||
// Always show manual path selection, but disable/enable based on useDefaultPath
|
||||
if (manualSelection) {
|
||||
manualSelection.style.display = 'block';
|
||||
if (this.useDefaultPath) {
|
||||
manualSelection.classList.add('disabled');
|
||||
// Disable all inputs and buttons inside manualSelection
|
||||
manualSelection.querySelectorAll('input, select, button').forEach(el => {
|
||||
el.disabled = true;
|
||||
el.tabIndex = -1;
|
||||
});
|
||||
} else {
|
||||
manualSelection.classList.remove('disabled');
|
||||
manualSelection.querySelectorAll('input, select, button').forEach(el => {
|
||||
el.disabled = false;
|
||||
el.tabIndex = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if modal is actually visible
|
||||
const modalDisplay = window.getComputedStyle(importModal).display;
|
||||
if (modalDisplay !== 'block') {
|
||||
console.error('Import modal is not visible, display: ' + modalDisplay);
|
||||
return false;
|
||||
// Always update the main path display
|
||||
this.updateTargetPath();
|
||||
}
|
||||
|
||||
async initializeFolderTree() {
|
||||
try {
|
||||
// Fetch unified folder tree
|
||||
const treeData = await this.apiClient.fetchUnifiedFolderTree();
|
||||
|
||||
if (treeData.success) {
|
||||
// Load tree data into folder tree manager
|
||||
await this.folderTreeManager.loadTree(treeData.tree);
|
||||
} else {
|
||||
console.error('Failed to fetch folder tree:', treeData.error);
|
||||
showToast('Failed to load folder tree', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing folder tree:', error);
|
||||
showToast('Error loading folder tree', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
cleanupFolderBrowser() {
|
||||
if (this.folderTreeManager) {
|
||||
this.folderTreeManager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
updateTargetPath() {
|
||||
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
||||
|
||||
return true;
|
||||
let fullPath = loraRoot || 'Select a LoRA root directory';
|
||||
|
||||
if (loraRoot) {
|
||||
if (this.useDefaultPath) {
|
||||
// Show actual template path
|
||||
try {
|
||||
const templates = state.global.settings.download_path_templates;
|
||||
const template = templates.lora;
|
||||
fullPath += `/${template}`;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch template:', error);
|
||||
fullPath += '/[Auto-organized by path template]';
|
||||
}
|
||||
} else {
|
||||
// Show manual path selection
|
||||
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : '';
|
||||
if (selectedPath) {
|
||||
fullPath += '/' + selectedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pathDisplay) {
|
||||
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
||||
import { getStorageItem } from '../../utils/storageHelpers.js';
|
||||
|
||||
export class DownloadManager {
|
||||
constructor(importManager) {
|
||||
@@ -120,14 +121,9 @@ export class DownloadManager {
|
||||
}
|
||||
|
||||
// Build target path
|
||||
let targetPath = loraRoot;
|
||||
let targetPath = '';
|
||||
if (this.importManager.selectedFolder) {
|
||||
targetPath += '/' + this.importManager.selectedFolder;
|
||||
}
|
||||
|
||||
const newFolder = document.getElementById('importNewFolder')?.value?.trim();
|
||||
if (newFolder) {
|
||||
targetPath += '/' + newFolder;
|
||||
targetPath = this.importManager.selectedFolder;
|
||||
}
|
||||
|
||||
// Generate a unique ID for this batch download
|
||||
@@ -189,6 +185,8 @@ export class DownloadManager {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const useDefaultPaths = getStorageItem('use_default_path_loras', false);
|
||||
|
||||
for (let i = 0; i < this.importManager.downloadableLoRAs.length; i++) {
|
||||
const lora = this.importManager.downloadableLoRAs[i];
|
||||
@@ -207,6 +205,7 @@ export class DownloadManager {
|
||||
lora.id,
|
||||
loraRoot,
|
||||
targetPath.replace(loraRoot + '/', ''),
|
||||
useDefaultPaths,
|
||||
batchDownloadId
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<div id="importModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="close" onclick="modalManager.closeModal('importModal')">×</button>
|
||||
<h2>Import Recipe</h2>
|
||||
<div class="modal-header">
|
||||
<button class="close" onclick="modalManager.closeModal('importModal')">×</button>
|
||||
<h2>Import Recipe</h2>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Upload Image or Input URL -->
|
||||
<div class="import-step" id="uploadStep">
|
||||
@@ -99,42 +101,59 @@
|
||||
<!-- Step 3: Download Location (if needed) -->
|
||||
<div class="import-step" id="locationStep" style="display: none;">
|
||||
<div class="location-selection">
|
||||
<!-- Improved missing LoRAs summary section -->
|
||||
<div class="missing-loras-summary">
|
||||
<div class="summary-header">
|
||||
<h3>Missing LoRAs <span class="lora-count-badge">(0)</span> <span id="totalDownloadSize" class="total-size-badge">Calculating...</span></h3>
|
||||
<button id="toggleMissingLorasList" class="toggle-list-btn">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="missingLorasList" class="missing-loras-list collapsed">
|
||||
<!-- Missing LoRAs will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move path preview to top -->
|
||||
<!-- Path preview with inline toggle -->
|
||||
<div class="path-preview">
|
||||
<label>Download Location Preview:</label>
|
||||
<div class="path-preview-header">
|
||||
<label>Download Location Preview:</label>
|
||||
<div class="inline-toggle-container" title="When enabled, files are automatically organized using configured path templates">
|
||||
<span class="inline-toggle-label">Use Default Path</span>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" id="importUseDefaultPath">
|
||||
<label for="importUseDefaultPath" class="toggle-slider"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="path-display" id="importTargetPathDisplay">
|
||||
<span class="path-text">Select a LoRA root directory</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Root Selection -->
|
||||
<div class="input-group">
|
||||
<label>Select LoRA Root:</label>
|
||||
<label for="importLoraRoot">Select LoRA Root:</label>
|
||||
<select id="importLoraRoot"></select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Target Folder:</label>
|
||||
<div class="folder-browser" id="importFolderBrowser">
|
||||
<!-- Folders will be populated here -->
|
||||
|
||||
<!-- Manual Path Selection -->
|
||||
<div class="manual-path-selection" id="importManualPathSelection">
|
||||
<!-- Path input with autocomplete -->
|
||||
<div class="input-group">
|
||||
<label for="importFolderPath">Target Folder Path:</label>
|
||||
<div class="path-input-container">
|
||||
<input type="text" id="importFolderPath" placeholder="Type folder path or select from tree below..." autocomplete="off" />
|
||||
<button type="button" id="importCreateFolderBtn" class="create-folder-btn" title="Create new folder">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="path-suggestions" id="importPathSuggestions" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumb navigation -->
|
||||
<div class="breadcrumb-nav" id="importBreadcrumbNav">
|
||||
<span class="breadcrumb-item root" data-path="">
|
||||
<i class="fas fa-home"></i> Root
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Hierarchical folder tree -->
|
||||
<div class="input-group">
|
||||
<label>Browse Folders:</label>
|
||||
<div class="folder-tree-container">
|
||||
<div class="folder-tree" id="importFolderTree">
|
||||
<!-- Tree will be loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="importNewFolder">New Folder (optional):</label>
|
||||
<input type="text" id="importNewFolder" placeholder="Enter folder name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user