refactor: Clean up and optimize import modal and related components, removing unused styles and improving path selection functionality

This commit is contained in:
Will Miao
2025-08-20 23:12:38 +08:00
parent af4cbe2332
commit 03b6f4b378
7 changed files with 235 additions and 176 deletions

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>`;
}
}
/**

View File

@@ -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
);

View File

@@ -1,7 +1,9 @@
<div id="importModal" class="modal">
<div class="modal-content">
<button class="close" onclick="modalManager.closeModal('importModal')">&times;</button>
<h2>Import Recipe</h2>
<div class="modal-header">
<button class="close" onclick="modalManager.closeModal('importModal')">&times;</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>