feat: add default path toggle and update download modal for improved path selection

This commit is contained in:
Will Miao
2025-08-13 23:15:02 +08:00
parent a141384907
commit 8d01d04ef0
4 changed files with 231 additions and 50 deletions

View File

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

View File

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

View File

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

View File

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