mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: add default path toggle and update download modal for improved path selection
This commit is contained in:
@@ -395,9 +395,16 @@
|
|||||||
border: 1px dashed var(--border-color);
|
border: 1px dashed var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-preview label {
|
.path-preview-header {
|
||||||
display: block;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-preview-header label {
|
||||||
|
margin: 0;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@@ -416,6 +423,70 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
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 */
|
/* Dark theme adjustments */
|
||||||
[data-theme="dark"] .version-item {
|
[data-theme="dark"] .version-item {
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
@@ -426,8 +497,18 @@
|
|||||||
border-color: var(--lora-border);
|
border-color: var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .toggle-slider:before {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Enhance the local badge to make it more noticeable */
|
/* Enhance the local badge to make it more noticeable */
|
||||||
.version-item.exists-locally {
|
.version-item.exists-locally {
|
||||||
background: oklch(var(--lora-accent) / 0.05);
|
background: oklch(var(--lora-accent) / 0.05);
|
||||||
border-left: 4px solid var(--lora-accent);
|
border-left: 4px solid var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-path-selection.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -613,7 +613,7 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadModel(modelId, versionId, modelRoot, relativePath, downloadId) {
|
async downloadModel(modelId, versionId, modelRoot, relativePath, useDefaultPaths = false, downloadId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(DOWNLOAD_ENDPOINTS.download, {
|
const response = await fetch(DOWNLOAD_ENDPOINTS.download, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -623,6 +623,7 @@ export class BaseModelApiClient {
|
|||||||
model_version_id: versionId,
|
model_version_id: versionId,
|
||||||
model_root: modelRoot,
|
model_root: modelRoot,
|
||||||
relative_path: relativePath,
|
relative_path: relativePath,
|
||||||
|
use_default_paths: useDefaultPaths,
|
||||||
download_id: downloadId
|
download_id: downloadId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
import { LoadingManager } from './LoadingManager.js';
|
import { LoadingManager } from './LoadingManager.js';
|
||||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||||
@@ -16,7 +17,8 @@ export class DownloadManager {
|
|||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
this.selectedFolder = '';
|
this.selectedFolder = '';
|
||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
|
this.useDefaultPath = false;
|
||||||
|
|
||||||
this.loadingManager = new LoadingManager();
|
this.loadingManager = new LoadingManager();
|
||||||
this.folderTreeManager = new FolderTreeManager();
|
this.folderTreeManager = new FolderTreeManager();
|
||||||
this.folderClickHandler = null;
|
this.folderClickHandler = null;
|
||||||
@@ -29,6 +31,7 @@ export class DownloadManager {
|
|||||||
this.handleBackToUrl = this.backToUrl.bind(this);
|
this.handleBackToUrl = this.backToUrl.bind(this);
|
||||||
this.handleBackToVersions = this.backToVersions.bind(this);
|
this.handleBackToVersions = this.backToVersions.bind(this);
|
||||||
this.handleCloseModal = this.closeModal.bind(this);
|
this.handleCloseModal = this.closeModal.bind(this);
|
||||||
|
this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDownloadModal() {
|
showDownloadModal() {
|
||||||
@@ -73,6 +76,9 @@ export class DownloadManager {
|
|||||||
document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl);
|
document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl);
|
||||||
document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions);
|
document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions);
|
||||||
document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal);
|
document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal);
|
||||||
|
|
||||||
|
// Default path toggle handler
|
||||||
|
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateModalLabels() {
|
updateModalLabels() {
|
||||||
@@ -126,6 +132,9 @@ export class DownloadManager {
|
|||||||
if (this.folderTreeManager) {
|
if (this.folderTreeManager) {
|
||||||
this.folderTreeManager.clearSelection();
|
this.folderTreeManager.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset default path toggle
|
||||||
|
this.loadDefaultPathSetting();
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateAndFetchVersions() {
|
async validateAndFetchVersions() {
|
||||||
@@ -329,12 +338,63 @@ export class DownloadManager {
|
|||||||
this.updateTargetPath();
|
this.updateTargetPath();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load default path setting for current model type
|
||||||
|
this.loadDefaultPathSetting();
|
||||||
|
|
||||||
this.updateTargetPath();
|
this.updateTargetPath();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message, '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() {
|
backToUrl() {
|
||||||
document.getElementById('versionStep').style.display = 'none';
|
document.getElementById('versionStep').style.display = 'none';
|
||||||
document.getElementById('urlStep').style.display = 'block';
|
document.getElementById('urlStep').style.display = 'block';
|
||||||
@@ -362,8 +422,16 @@ export class DownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selected folder path from folder tree manager
|
// Determine target folder and use_default_paths parameter
|
||||||
const targetFolder = this.folderTreeManager.getSelectedPath();
|
let targetFolder = '';
|
||||||
|
let useDefaultPaths = false;
|
||||||
|
|
||||||
|
if (this.useDefaultPath) {
|
||||||
|
useDefaultPaths = true;
|
||||||
|
targetFolder = ''; // Not needed when using default paths
|
||||||
|
} else {
|
||||||
|
targetFolder = this.folderTreeManager.getSelectedPath();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateProgress = this.loadingManager.showDownloadProgress(1);
|
const updateProgress = this.loadingManager.showDownloadProgress(1);
|
||||||
@@ -402,12 +470,13 @@ export class DownloadManager {
|
|||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start download
|
// Start download with use_default_paths parameter
|
||||||
await this.apiClient.downloadModel(
|
await this.apiClient.downloadModel(
|
||||||
this.modelId,
|
this.modelId,
|
||||||
this.currentVersion.id,
|
this.currentVersion.id,
|
||||||
modelRoot,
|
modelRoot,
|
||||||
targetFolder,
|
targetFolder,
|
||||||
|
useDefaultPaths,
|
||||||
downloadId
|
downloadId
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -418,19 +487,22 @@ export class DownloadManager {
|
|||||||
|
|
||||||
// Update state and trigger reload
|
// Update state and trigger reload
|
||||||
const pageState = this.apiClient.getPageState();
|
const pageState = this.apiClient.getPageState();
|
||||||
pageState.activeFolder = targetFolder;
|
|
||||||
|
|
||||||
// Save the active folder preference
|
if (!useDefaultPaths) {
|
||||||
setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder);
|
pageState.activeFolder = targetFolder;
|
||||||
|
|
||||||
// Update UI folder selection
|
// Save the active folder preference
|
||||||
document.querySelectorAll('.folder-tags .tag').forEach(tag => {
|
setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder);
|
||||||
const isActive = tag.dataset.folder === targetFolder;
|
|
||||||
tag.classList.toggle('active', isActive);
|
// Update UI folder selection
|
||||||
if (isActive && !tag.parentNode.classList.contains('collapsed')) {
|
document.querySelectorAll('.folder-tags .tag').forEach(tag => {
|
||||||
tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
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);
|
await resetAndReload(true);
|
||||||
|
|
||||||
@@ -517,9 +589,23 @@ export class DownloadManager {
|
|||||||
let fullPath = modelRoot || `Select a ${config.displayName} root directory`;
|
let fullPath = modelRoot || `Select a ${config.displayName} root directory`;
|
||||||
|
|
||||||
if (modelRoot) {
|
if (modelRoot) {
|
||||||
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : '';
|
if (this.useDefaultPath) {
|
||||||
if (selectedPath) {
|
// Show actual template path
|
||||||
fullPath += '/' + selectedPath;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,44 +32,57 @@
|
|||||||
<!-- Step 3: Location Selection -->
|
<!-- Step 3: Location Selection -->
|
||||||
<div class="download-step" id="locationStep" style="display: none;">
|
<div class="download-step" id="locationStep" style="display: none;">
|
||||||
<div class="location-selection">
|
<div class="location-selection">
|
||||||
<!-- Path preview -->
|
<!-- Path preview with inline toggle -->
|
||||||
<div class="path-preview">
|
<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="useDefaultPath">
|
||||||
|
<label for="useDefaultPath" class="toggle-slider"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="path-display" id="targetPathDisplay">
|
<div class="path-display" id="targetPathDisplay">
|
||||||
<span class="path-text">Select a root directory</span>
|
<span class="path-text">Select a root directory</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Root Selection (always visible) -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="modelRoot" id="modelRootLabel">Select Model Root:</label>
|
<label for="modelRoot" id="modelRootLabel">Select Model Root:</label>
|
||||||
<select id="modelRoot"></select>
|
<select id="modelRoot"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Path input with autocomplete -->
|
<!-- Manual Path Selection (hidden when using default path) -->
|
||||||
<div class="input-group">
|
<div class="manual-path-selection" id="manualPathSelection">
|
||||||
<label for="folderPath">Target Folder Path:</label>
|
<!-- Path input with autocomplete -->
|
||||||
<div class="path-input-container">
|
<div class="input-group">
|
||||||
<input type="text" id="folderPath" placeholder="Type folder path or select from tree below..." autocomplete="off" />
|
<label for="folderPath">Target Folder Path:</label>
|
||||||
<button type="button" id="createFolderBtn" class="create-folder-btn" title="Create new folder">
|
<div class="path-input-container">
|
||||||
<i class="fas fa-plus"></i>
|
<input type="text" id="folderPath" placeholder="Type folder path or select from tree below..." autocomplete="off" />
|
||||||
</button>
|
<button type="button" id="createFolderBtn" class="create-folder-btn" title="Create new folder">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="path-suggestions" id="pathSuggestions" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="path-suggestions" id="pathSuggestions" style="display: none;"></div>
|
|
||||||
</div>
|
<!-- Breadcrumb navigation -->
|
||||||
|
<div class="breadcrumb-nav" id="breadcrumbNav">
|
||||||
<!-- Breadcrumb navigation -->
|
<span class="breadcrumb-item root" data-path="">
|
||||||
<div class="breadcrumb-nav" id="breadcrumbNav">
|
<i class="fas fa-home"></i> Root
|
||||||
<span class="breadcrumb-item root" data-path="">
|
</span>
|
||||||
<i class="fas fa-home"></i> Root
|
</div>
|
||||||
</span>
|
|
||||||
</div>
|
<!-- Hierarchical folder tree -->
|
||||||
|
<div class="input-group">
|
||||||
<!-- Hierarchical folder tree -->
|
<label>Browse Folders:</label>
|
||||||
<div class="input-group">
|
<div class="folder-tree-container">
|
||||||
<label>Browse Folders:</label>
|
<div class="folder-tree" id="folderTree">
|
||||||
<div class="folder-tree-container">
|
<!-- Tree will be loaded dynamically -->
|
||||||
<div class="folder-tree" id="folderTree">
|
</div>
|
||||||
<!-- Tree will be loaded dynamically -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user