diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index 9d717c0d..2699d697 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -395,9 +395,16 @@ border: 1px dashed var(--border-color); } -.path-preview label { - display: block; - margin-bottom: 8px; +.path-preview-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + gap: var(--space-2); +} + +.path-preview-header label { + margin: 0; color: var(--text-color); font-size: 0.9em; opacity: 0.8; @@ -416,6 +423,70 @@ border-radius: var(--border-radius-xs); } +/* Inline Toggle Styles */ +.inline-toggle-container { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + position: relative; +} + +.inline-toggle-label { + font-size: 0.85em; + color: var(--text-color); + opacity: 0.9; + white-space: nowrap; +} + +.inline-toggle-container .toggle-switch { + position: relative; + width: 36px; + height: 18px; + flex-shrink: 0; +} + +.inline-toggle-container .toggle-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.inline-toggle-container .toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--border-color); + transition: all 0.3s ease; + border-radius: 18px; +} + +.inline-toggle-container .toggle-slider:before { + position: absolute; + content: ""; + height: 12px; + width: 12px; + left: 3px; + bottom: 3px; + background-color: white; + transition: all 0.3s ease; + border-radius: 50%; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.inline-toggle-container .toggle-switch input:checked + .toggle-slider { + background-color: var(--lora-accent); +} + +.inline-toggle-container .toggle-switch input:checked + .toggle-slider:before { + transform: translateX(18px); +} + /* Dark theme adjustments */ [data-theme="dark"] .version-item { background: var(--lora-surface); @@ -426,8 +497,18 @@ border-color: var(--lora-border); } +[data-theme="dark"] .toggle-slider:before { + background-color: #f0f0f0; +} + /* Enhance the local badge to make it more noticeable */ .version-item.exists-locally { background: oklch(var(--lora-accent) / 0.05); border-left: 4px solid var(--lora-accent); +} + +.manual-path-selection.disabled { + opacity: 0.5; + pointer-events: none; + user-select: none; } \ No newline at end of file diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index dc5f825e..90db4234 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -613,7 +613,7 @@ export class BaseModelApiClient { } } - async downloadModel(modelId, versionId, modelRoot, relativePath, downloadId) { + async downloadModel(modelId, versionId, modelRoot, relativePath, useDefaultPaths = false, downloadId) { try { const response = await fetch(DOWNLOAD_ENDPOINTS.download, { method: 'POST', @@ -623,6 +623,7 @@ export class BaseModelApiClient { model_version_id: versionId, model_root: modelRoot, relative_path: relativePath, + use_default_paths: useDefaultPaths, download_id: downloadId }) }); diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index bbec8781..19849c23 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -1,5 +1,6 @@ import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; +import { state } from '../state/index.js'; import { LoadingManager } from './LoadingManager.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; @@ -16,7 +17,8 @@ export class DownloadManager { this.initialized = false; this.selectedFolder = ''; this.apiClient = null; - + this.useDefaultPath = false; + this.loadingManager = new LoadingManager(); this.folderTreeManager = new FolderTreeManager(); this.folderClickHandler = null; @@ -29,6 +31,7 @@ export class DownloadManager { this.handleBackToUrl = this.backToUrl.bind(this); this.handleBackToVersions = this.backToVersions.bind(this); this.handleCloseModal = this.closeModal.bind(this); + this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this); } showDownloadModal() { @@ -73,6 +76,9 @@ export class DownloadManager { document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl); document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions); document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal); + + // Default path toggle handler + document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath); } updateModalLabels() { @@ -126,6 +132,9 @@ export class DownloadManager { if (this.folderTreeManager) { this.folderTreeManager.clearSelection(); } + + // Reset default path toggle + this.loadDefaultPathSetting(); } async validateAndFetchVersions() { @@ -329,12 +338,63 @@ export class DownloadManager { this.updateTargetPath(); }); + // Load default path setting for current model type + this.loadDefaultPathSetting(); + this.updateTargetPath(); } catch (error) { showToast(error.message, 'error'); } } + loadDefaultPathSetting() { + const modelType = this.apiClient.modelType; + const storageKey = `use_default_path_${modelType}`; + this.useDefaultPath = getStorageItem(storageKey, false); + + const toggleInput = document.getElementById('useDefaultPath'); + if (toggleInput) { + toggleInput.checked = this.useDefaultPath; + this.updatePathSelectionUI(); + } + } + + toggleDefaultPath(event) { + this.useDefaultPath = event.target.checked; + + // Save to localStorage per model type + const modelType = this.apiClient.modelType; + const storageKey = `use_default_path_${modelType}`; + setStorageItem(storageKey, this.useDefaultPath); + + this.updatePathSelectionUI(); + this.updateTargetPath(); + } + + updatePathSelectionUI() { + const manualSelection = document.getElementById('manualPathSelection'); + + // Always show manual path selection, but disable/enable based on useDefaultPath + 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; + }); + } + + // Always update the main path display + this.updateTargetPath(); + } + backToUrl() { document.getElementById('versionStep').style.display = 'none'; document.getElementById('urlStep').style.display = 'block'; @@ -362,8 +422,16 @@ export class DownloadManager { return; } - // Get selected folder path from folder tree manager - const targetFolder = this.folderTreeManager.getSelectedPath(); + // Determine target folder and use_default_paths parameter + let targetFolder = ''; + let useDefaultPaths = false; + + if (this.useDefaultPath) { + useDefaultPaths = true; + targetFolder = ''; // Not needed when using default paths + } else { + targetFolder = this.folderTreeManager.getSelectedPath(); + } try { const updateProgress = this.loadingManager.showDownloadProgress(1); @@ -402,12 +470,13 @@ export class DownloadManager { console.error('WebSocket error:', error); }; - // Start download + // Start download with use_default_paths parameter await this.apiClient.downloadModel( this.modelId, this.currentVersion.id, modelRoot, targetFolder, + useDefaultPaths, downloadId ); @@ -418,19 +487,22 @@ export class DownloadManager { // Update state and trigger reload const pageState = this.apiClient.getPageState(); - pageState.activeFolder = targetFolder; - // Save the active folder preference - setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder); - - // Update UI folder selection - document.querySelectorAll('.folder-tags .tag').forEach(tag => { - const isActive = tag.dataset.folder === targetFolder; - tag.classList.toggle('active', isActive); - if (isActive && !tag.parentNode.classList.contains('collapsed')) { - tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - }); + if (!useDefaultPaths) { + pageState.activeFolder = targetFolder; + + // Save the active folder preference + setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder); + + // Update UI folder selection + document.querySelectorAll('.folder-tags .tag').forEach(tag => { + const isActive = tag.dataset.folder === targetFolder; + tag.classList.toggle('active', isActive); + if (isActive && !tag.parentNode.classList.contains('collapsed')) { + tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }); + } await resetAndReload(true); @@ -517,9 +589,23 @@ export class DownloadManager { let fullPath = modelRoot || `Select a ${config.displayName} root directory`; if (modelRoot) { - const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : ''; - if (selectedPath) { - fullPath += '/' + selectedPath; + if (this.useDefaultPath) { + // Show actual template path + try { + const singularType = this.apiClient.modelType.replace(/s$/, ''); + const templates = state.global.settings.download_path_templates; + const template = templates[singularType]; + 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; + } } } diff --git a/templates/components/modals/download_modal.html b/templates/components/modals/download_modal.html index 019458d7..69516318 100644 --- a/templates/components/modals/download_modal.html +++ b/templates/components/modals/download_modal.html @@ -32,44 +32,57 @@