From 03b6f4b3784a58bbbc81a170afd7922bfe47611e Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 20 Aug 2025 23:12:38 +0800 Subject: [PATCH] refactor: Clean up and optimize import modal and related components, removing unused styles and improving path selection functionality --- py/utils/routes_common.py | 9 - static/css/components/import-modal.css | 81 ------- static/css/components/modal/_base.css | 2 +- .../css/components/modal/download-modal.css | 9 - static/js/managers/ImportManager.js | 222 ++++++++++++++---- static/js/managers/import/DownloadManager.js | 13 +- templates/components/import_modal.html | 75 +++--- 7 files changed, 235 insertions(+), 176 deletions(-) diff --git a/py/utils/routes_common.py b/py/utils/routes_common.py index 1b583d72..e961d81c 100644 --- a/py/utils/routes_common.py +++ b/py/utils/routes_common.py @@ -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, diff --git a/static/css/components/import-modal.css b/static/css/components/import-modal.css index a876e344..82c34672 100644 --- a/static/css/components/import-modal.css +++ b/static/css/components/import-modal.css @@ -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); diff --git a/static/css/components/modal/_base.css b/static/css/components/modal/_base.css index cfc172f9..073692b2 100644 --- a/static/css/components/modal/_base.css +++ b/static/css/components/modal/_base.css @@ -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); diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index a3d99641..09d0143f 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -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; diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index 926720c9..1681b478 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -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 = '
No tags added
'; + // 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 => + `` + ).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 = `${fullPath}`; + } } /** diff --git a/static/js/managers/import/DownloadManager.js b/static/js/managers/import/DownloadManager.js index 55fb3d38..8ca178b2 100644 --- a/static/js/managers/import/DownloadManager.js +++ b/static/js/managers/import/DownloadManager.js @@ -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 ); diff --git a/templates/components/import_modal.html b/templates/components/import_modal.html index 243b5cc5..20aef17a 100644 --- a/templates/components/import_modal.html +++ b/templates/components/import_modal.html @@ -1,7 +1,9 @@