feat: Implement model move, import, and download functionalities with corresponding UI and API updates.

This commit is contained in:
Will Miao
2025-12-28 21:18:27 +08:00
parent e5b557504e
commit 5d5a2a998a
9 changed files with 420 additions and 308 deletions

View File

@@ -15,17 +15,17 @@ export class DownloadManager {
this.modelVersionId = null;
this.modelId = null;
this.source = null;
this.initialized = false;
this.selectedFolder = '';
this.apiClient = null;
this.useDefaultPath = false;
this.loadingManager = new LoadingManager();
this.folderTreeManager = new FolderTreeManager();
this.folderClickHandler = null;
this.updateTargetPath = this.updateTargetPath.bind(this);
// Bound methods for event handling
this.handleValidateAndFetchVersions = this.validateAndFetchVersions.bind(this);
this.handleProceedToLocation = this.proceedToLocation.bind(this);
@@ -38,11 +38,11 @@ export class DownloadManager {
showDownloadModal() {
console.log('Showing unified download modal...');
// Get API client for current page type
this.apiClient = getModelApiClient();
const config = this.apiClient.apiConfig.config;
if (!this.initialized) {
const modal = document.getElementById('downloadModal');
if (!modal) {
@@ -52,15 +52,15 @@ export class DownloadManager {
this.initializeEventHandlers();
this.initialized = true;
}
// Update modal title and labels based on model type
this.updateModalLabels();
modalManager.showModal('downloadModal', null, () => {
this.cleanupFolderBrowser();
});
this.resetSteps();
// Auto-focus on the URL input
setTimeout(() => {
const urlInput = document.getElementById('modelUrl');
@@ -78,23 +78,23 @@ 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() {
const config = this.apiClient.apiConfig.config;
// Update modal title
document.getElementById('downloadModalTitle').textContent = translate('modals.download.titleWithType', { type: config.displayName });
// Update URL label
document.getElementById('modelUrlLabel').textContent = translate('modals.download.civitaiUrl');
// Update root selection label
document.getElementById('modelRootLabel').textContent = translate('modals.download.selectTypeRoot', { type: config.displayName });
// Update path preview labels
const pathLabels = document.querySelectorAll('.path-preview label');
pathLabels.forEach(label => {
@@ -102,7 +102,7 @@ export class DownloadManager {
label.textContent = translate('modals.download.locationPreview') + ':';
}
});
// Update initial path text
const pathText = document.querySelector('#targetPathDisplay .path-text');
if (pathText) {
@@ -115,27 +115,27 @@ export class DownloadManager {
document.getElementById('urlStep').style.display = 'block';
document.getElementById('modelUrl').value = '';
document.getElementById('urlError').textContent = '';
// Clear folder path input
const folderPathInput = document.getElementById('folderPath');
if (folderPathInput) {
folderPathInput.value = '';
}
this.currentVersion = null;
this.versions = [];
this.modelInfo = null;
this.modelId = null;
this.modelVersionId = null;
this.source = null;
this.selectedFolder = '';
// Clear folder tree selection
if (this.folderTreeManager) {
this.folderTreeManager.clearSelection();
}
// Reset default path toggle
this.loadDefaultPathSetting();
}
@@ -151,10 +151,10 @@ export class DownloadManager {
async validateAndFetchVersions() {
const url = document.getElementById('modelUrl').value.trim();
const errorElement = document.getElementById('urlError');
try {
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
this.modelId = this.extractModelId(url);
if (!this.modelId) {
throw new Error(translate('modals.download.errors.invalidUrl'));
@@ -166,7 +166,7 @@ export class DownloadManager {
if (this.modelVersionId) {
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
}
this.showVersionStep();
} catch (error) {
errorElement.textContent = error.message;
@@ -239,20 +239,20 @@ export class DownloadManager {
showVersionStep() {
document.getElementById('urlStep').style.display = 'none';
document.getElementById('versionStep').style.display = 'block';
const versionList = document.getElementById('versionList');
versionList.innerHTML = this.versions.map(version => {
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
const fileSize = version.modelSizeKB ?
(version.modelSizeKB / 1024).toFixed(2) :
const fileSize = version.modelSizeKB ?
(version.modelSizeKB / 1024).toFixed(2) :
(version.files[0]?.sizeKB / 1024).toFixed(2);
const existsLocally = version.existsLocally;
const localPath = version.localPath;
const isEarlyAccess = version.availability === 'EarlyAccess';
let earlyAccessBadge = '';
if (isEarlyAccess) {
earlyAccessBadge = `
@@ -261,8 +261,8 @@ export class DownloadManager {
</div>
`;
}
const localStatus = existsLocally ?
const localStatus = existsLocally ?
`<div class="local-badge">
<i class="fas fa-check"></i> ${translate('modals.download.inLibrary')}
<div class="local-path">${localPath || ''}</div>
@@ -293,7 +293,7 @@ export class DownloadManager {
</div>
`;
}).join('');
// Add click handlers for version selection
versionList.addEventListener('click', (event) => {
const versionItem = event.target.closest('.version-item');
@@ -301,12 +301,12 @@ export class DownloadManager {
this.selectVersion(versionItem.dataset.versionId);
}
});
// Auto-select the version if there's only one
if (this.versions.length === 1 && !this.currentVersion) {
this.selectVersion(this.versions[0].id.toString());
}
this.updateNextButtonState();
}
@@ -317,16 +317,16 @@ export class DownloadManager {
document.querySelectorAll('.version-item').forEach(item => {
item.classList.toggle('selected', item.dataset.versionId === versionId);
});
this.updateNextButtonState();
}
updateNextButtonState() {
const nextButton = document.getElementById('nextFromVersion');
if (!nextButton) return;
const existsLocally = this.currentVersion?.existsLocally;
if (existsLocally) {
nextButton.disabled = true;
nextButton.classList.add('disabled');
@@ -343,7 +343,7 @@ export class DownloadManager {
showToast('toast.loras.pleaseSelectVersion', {}, 'error');
return;
}
const existsLocally = this.currentVersion.existsLocally;
if (existsLocally) {
showToast('toast.loras.versionExists', {}, 'info');
@@ -352,12 +352,12 @@ export class DownloadManager {
document.getElementById('versionStep').style.display = 'none';
document.getElementById('locationStep').style.display = 'block';
try {
// Fetch model roots
const rootsData = await this.apiClient.fetchModelRoots();
const modelRoot = document.getElementById('modelRoot');
modelRoot.innerHTML = rootsData.roots.map(root =>
modelRoot.innerHTML = rootsData.roots.map(root =>
`<option value="${root}">${root}</option>`
).join('');
@@ -380,7 +380,7 @@ export class DownloadManager {
// Initialize folder tree
await this.initializeFolderTree();
// Setup folder tree manager
this.folderTreeManager.init({
onPathChange: (path) => {
@@ -388,16 +388,16 @@ export class DownloadManager {
this.updateTargetPath();
}
});
// Setup model root change handler
modelRoot.addEventListener('change', async () => {
await this.initializeFolderTree();
this.updateTargetPath();
});
// Load default path setting for current model type
this.loadDefaultPathSetting();
this.updateTargetPath();
} catch (error) {
showToast('toast.downloads.loadError', { message: error.message }, 'error');
@@ -408,7 +408,7 @@ export class DownloadManager {
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;
@@ -418,12 +418,12 @@ export class DownloadManager {
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();
}
@@ -446,7 +446,7 @@ export class DownloadManager {
const displayName = versionName || `#${versionId}`;
let ws = null;
let updateProgress = () => {};
let updateProgress = () => { };
try {
this.loadingManager.restoreProgressBar();
@@ -549,7 +549,7 @@ export class DownloadManager {
updatePathSelectionUI() {
const manualSelection = document.getElementById('manualPathSelection');
// Always show manual path selection, but disable/enable based on useDefaultPath
manualSelection.style.display = 'block';
if (this.useDefaultPath) {
@@ -566,11 +566,11 @@ export class DownloadManager {
el.tabIndex = 0;
});
}
// Always update the main path display
this.updateTargetPath();
}
backToUrl() {
document.getElementById('versionStep').style.display = 'none';
document.getElementById('urlStep').style.display = 'block';
@@ -592,7 +592,7 @@ export class DownloadManager {
async startDownload() {
const modelRoot = document.getElementById('modelRoot').value;
const config = this.apiClient.apiConfig.config;
if (!modelRoot) {
showToast('toast.models.pleaseSelectRoot', { type: config.displayName }, 'error');
return;
@@ -601,7 +601,7 @@ export class DownloadManager {
// 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
@@ -646,7 +646,7 @@ export class DownloadManager {
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);
@@ -674,23 +674,23 @@ export class DownloadManager {
folderItem.classList.remove('selected');
this.selectedFolder = '';
} else {
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
f.classList.remove('selected'));
folderItem.classList.add('selected');
this.selectedFolder = folderItem.dataset.folder;
}
this.updateTargetPath();
};
folderBrowser.addEventListener('click', this.folderClickHandler);
const modelRoot = document.getElementById('modelRoot');
const newFolder = document.getElementById('newFolder');
modelRoot.addEventListener('change', this.updateTargetPath);
newFolder.addEventListener('input', this.updateTargetPath);
this.updateTargetPath();
}
@@ -702,21 +702,21 @@ export class DownloadManager {
this.folderClickHandler = null;
}
}
const modelRoot = document.getElementById('modelRoot');
const newFolder = document.getElementById('newFolder');
if (modelRoot) modelRoot.removeEventListener('change', this.updateTargetPath);
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
}
updateTargetPath() {
const pathDisplay = document.getElementById('targetPathDisplay');
const modelRoot = document.getElementById('modelRoot').value;
const config = this.apiClient.apiConfig.config;
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName });
if (modelRoot) {
if (this.useDefaultPath) {
// Show actual template path

View File

@@ -28,7 +28,7 @@ export class ImportManager {
this.importMode = 'url'; // Default mode: 'url' or 'upload'
this.useDefaultPath = false;
this.apiClient = null;
// Initialize sub-managers
this.loadingManager = new LoadingManager();
this.stepManager = new ImportStepManager();
@@ -36,7 +36,7 @@ export class ImportManager {
this.recipeDataManager = new RecipeDataManager(this);
this.downloadManager = new DownloadManager(this);
this.folderTreeManager = new FolderTreeManager();
// Bind methods
this.formatFileSize = formatFileSize;
this.updateTargetPath = this.updateTargetPath.bind(this);
@@ -53,17 +53,17 @@ export class ImportManager {
this.initializeEventHandlers();
this.initialized = true;
}
// Get API client for LoRAs
this.apiClient = getModelApiClient(MODEL_TYPES.LORA);
// Reset state
this.resetSteps();
if (recipeData) {
this.downloadableLoRAs = recipeData.loras;
this.recipeId = recipeId;
}
// Show modal
modalManager.showModal('importModal', null, () => {
this.cleanupFolderBrowser();
@@ -71,7 +71,7 @@ export class ImportManager {
});
// Verify visibility and focus on URL input
setTimeout(() => {
setTimeout(() => {
// Ensure URL option is selected and focus on the input
this.toggleImportMode('url');
const urlInput = document.getElementById('imageUrlInput');
@@ -93,32 +93,32 @@ export class ImportManager {
// Clear UI state
this.stepManager.removeInjectedStyles();
this.stepManager.showStep('uploadStep');
// Reset form inputs
const fileInput = document.getElementById('recipeImageUpload');
if (fileInput) fileInput.value = '';
const urlInput = document.getElementById('imageUrlInput');
if (urlInput) urlInput.value = '';
const uploadError = document.getElementById('uploadError');
if (uploadError) uploadError.textContent = '';
const importUrlError = document.getElementById('importUrlError');
if (importUrlError) importUrlError.textContent = '';
const recipeName = document.getElementById('recipeName');
if (recipeName) recipeName.value = '';
const tagsContainer = document.getElementById('tagsContainer');
if (tagsContainer) tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, '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;
@@ -127,30 +127,30 @@ export class ImportManager {
this.missingLoras = [];
this.downloadableLoRAs = [];
this.selectedFolder = '';
// Reset import mode
this.importMode = 'url';
this.toggleImportMode('url');
// Clear folder tree selection
if (this.folderTreeManager) {
this.folderTreeManager.clearSelection();
}
// Reset default path toggle
this.loadDefaultPathSetting();
// Reset duplicate related properties
this.duplicateRecipes = [];
}
toggleImportMode(mode) {
this.importMode = mode;
// Update toggle buttons
const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]');
const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]');
if (uploadBtn && urlBtn) {
if (mode === 'upload') {
uploadBtn.classList.add('active');
@@ -160,11 +160,11 @@ export class ImportManager {
urlBtn.classList.add('active');
}
}
// Show/hide appropriate sections
const uploadSection = document.getElementById('uploadSection');
const urlSection = document.getElementById('urlSection');
if (uploadSection && urlSection) {
if (mode === 'upload') {
uploadSection.style.display = 'block';
@@ -174,11 +174,11 @@ export class ImportManager {
urlSection.style.display = 'block';
}
}
// Clear error messages
const uploadError = document.getElementById('uploadError');
const importUrlError = document.getElementById('importUrlError');
if (uploadError) uploadError.textContent = '';
if (importUrlError) importUrlError.textContent = '';
}
@@ -206,7 +206,7 @@ export class ImportManager {
addTag() {
this.recipeDataManager.addTag();
}
removeTag(tag) {
this.recipeDataManager.removeTag(tag);
}
@@ -217,12 +217,12 @@ export class ImportManager {
async 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 =>
loraRoot.innerHTML = rootsData.roots.map(root =>
`<option value="${root}">${root}</option>`
).join('');
@@ -247,19 +247,19 @@ export class ImportManager {
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('toast.recipes.importFailed', { message: error.message }, 'error');
@@ -268,19 +268,19 @@ export class ImportManager {
backToUpload() {
this.stepManager.showStep('uploadStep');
// Reset file input
const fileInput = document.getElementById('recipeImageUpload');
if (fileInput) fileInput.value = '';
// Reset URL input
const urlInput = document.getElementById('imageUrlInput');
if (urlInput) urlInput.value = '';
// Clear error messages
const uploadError = document.getElementById('uploadError');
if (uploadError) uploadError.textContent = '';
const importUrlError = document.getElementById('importUrlError');
if (importUrlError) importUrlError.textContent = '';
}
@@ -296,7 +296,7 @@ export class ImportManager {
loadDefaultPathSetting() {
const storageKey = 'use_default_path_loras';
this.useDefaultPath = getStorageItem(storageKey, false);
const toggleInput = document.getElementById('importUseDefaultPath');
if (toggleInput) {
toggleInput.checked = this.useDefaultPath;
@@ -306,18 +306,18 @@ export class ImportManager {
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';
@@ -336,7 +336,7 @@ export class ImportManager {
});
}
}
// Always update the main path display
this.updateTargetPath();
}
@@ -345,7 +345,7 @@ export class ImportManager {
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);
@@ -368,8 +368,8 @@ export class ImportManager {
updateTargetPath() {
const pathDisplay = document.getElementById('importTargetPathDisplay');
const loraRoot = document.getElementById('importLoraRoot').value;
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) {
let fullPath = loraRoot || translate('recipes.controls.import.selectLoraRoot', {}, 'Select a LoRA root directory'); if (loraRoot) {
if (this.useDefaultPath) {
// Show actual template path
try {
@@ -417,19 +417,19 @@ export class ImportManager {
// Store the recipe data and ID
this.recipeData = recipeData;
this.recipeId = recipeId;
// Show the modal and go to location step
this.showImportModal(recipeData, recipeId);
this.proceedToLocation();
// Update the modal title
const modalTitle = document.querySelector('#importModal h2');
if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
// Update the save button text
const saveButton = document.querySelector('#locationStep .primary-btn');
if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
// Hide the back button
const backButton = document.querySelector('#locationStep .secondary-btn');
if (backButton) backButton.style.display = 'none';

View File

@@ -6,6 +6,8 @@ import { getModelApiClient } from '../api/modelApiFactory.js';
import { RecipeSidebarApiClient } from '../api/recipeApi.js';
import { FolderTreeManager } from '../components/FolderTreeManager.js';
import { sidebarManager } from '../components/SidebarManager.js';
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import { translate } from '../utils/i18nHelpers.js';
class MoveManager {
constructor() {
@@ -14,9 +16,11 @@ class MoveManager {
this.folderTreeManager = new FolderTreeManager();
this.initialized = false;
this.recipeApiClient = null;
this.useDefaultPath = false;
// Bind methods
this.updateTargetPath = this.updateTargetPath.bind(this);
this.handleToggleDefaultPath = this.handleToggleDefaultPath.bind(this);
}
_getApiClient(modelType = null) {
@@ -31,15 +35,21 @@ class MoveManager {
initializeEventListeners() {
if (this.initialized) return;
const modelRootSelect = document.getElementById('moveModelRoot');
// Initialize model root directory selector
modelRootSelect.addEventListener('change', async () => {
await this.initializeFolderTree();
this.updateTargetPath();
});
// Default path toggle handler
const toggleInput = document.getElementById('moveUseDefaultPath');
if (toggleInput) {
toggleInput.addEventListener('change', this.handleToggleDefaultPath);
}
this.initialized = true;
}
@@ -47,11 +57,11 @@ class MoveManager {
// Reset state
this.currentFilePath = null;
this.bulkFilePaths = null;
const apiClient = this._getApiClient(modelType);
const currentPageType = state.currentPageType;
const modelConfig = apiClient.apiConfig.config;
// Handle bulk mode
if (filePath === 'bulk') {
const selectedPaths = Array.from(state.selectedModels);
@@ -66,11 +76,11 @@ class MoveManager {
this.currentFilePath = filePath;
document.getElementById('moveModalTitle').textContent = `Move ${modelConfig.displayName}`;
}
// Update UI labels based on model type
document.getElementById('moveRootLabel').textContent = `Select ${modelConfig.displayName} Root:`;
document.getElementById('moveTargetPathDisplay').querySelector('.path-text').textContent = `Select a ${modelConfig.displayName.toLowerCase()} root directory`;
// Clear folder path input
const folderPathInput = document.getElementById('moveFolderPath');
if (folderPathInput) {
@@ -86,13 +96,13 @@ class MoveManager {
} else {
rootsData = await apiClient.fetchModelRoots();
}
if (!rootsData.roots || rootsData.roots.length === 0) {
throw new Error(`No ${modelConfig.displayName.toLowerCase()} roots found`);
}
// Populate model root selector
modelRootSelect.innerHTML = rootsData.roots.map(root =>
modelRootSelect.innerHTML = rootsData.roots.map(root =>
`<option value="${root}">${root}</option>`
).join('');
@@ -105,7 +115,7 @@ class MoveManager {
// Initialize event listeners
this.initializeEventListeners();
// Setup folder tree manager
this.folderTreeManager.init({
onPathChange: (path) => {
@@ -113,10 +123,13 @@ class MoveManager {
},
elementsPrefix: 'move'
});
// Initialize folder tree
await this.initializeFolderTree();
// Load default path setting
this.loadDefaultPathSetting(apiClient.modelType);
this.updateTargetPath();
modalManager.showModal('moveModal', null, () => {
// Cleanup on modal close
@@ -124,19 +137,63 @@ class MoveManager {
this.folderTreeManager.destroy();
}
});
} catch (error) {
console.error(`Error fetching ${modelConfig.displayName.toLowerCase()} roots or folders:`, error);
showToast('toast.models.moveFailed', { message: error.message }, 'error');
}
}
loadDefaultPathSetting(modelType) {
const storageKey = `use_default_path_${modelType}`;
this.useDefaultPath = getStorageItem(storageKey, false);
const toggleInput = document.getElementById('moveUseDefaultPath');
if (toggleInput) {
toggleInput.checked = this.useDefaultPath;
this.updatePathSelectionUI();
}
}
handleToggleDefaultPath(event) {
this.useDefaultPath = event.target.checked;
// Save to localStorage per model type
const apiClient = this._getApiClient();
const modelType = apiClient.modelType;
const storageKey = `use_default_path_${modelType}`;
setStorageItem(storageKey, this.useDefaultPath);
this.updatePathSelectionUI();
this.updateTargetPath();
}
updatePathSelectionUI() {
const manualSelection = document.getElementById('moveManualPathSelection');
if (!manualSelection) return;
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;
});
}
}
async initializeFolderTree() {
try {
const apiClient = this._getApiClient();
// Fetch unified folder tree
const treeData = await apiClient.fetchUnifiedFolderTree();
if (treeData.success) {
// Load tree data into folder tree manager
await this.folderTreeManager.loadTree(treeData.tree);
@@ -155,13 +212,27 @@ class MoveManager {
const modelRoot = document.getElementById('moveModelRoot').value;
const apiClient = this._getApiClient();
const config = apiClient.apiConfig.config;
let fullPath = modelRoot || `Select a ${config.displayName.toLowerCase()} root directory`;
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName });
if (modelRoot) {
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : '';
if (selectedPath) {
fullPath += '/' + selectedPath;
if (this.useDefaultPath) {
// Show actual template path
try {
const singularType = 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 += '/' + translate('modals.download.autoOrganizedPath');
}
} else {
// Show manual path selection
const selectedPath = this.folderTreeManager ? this.folderTreeManager.getSelectedPath() : '';
if (selectedPath) {
fullPath += '/' + selectedPath;
}
}
}
@@ -172,7 +243,7 @@ class MoveManager {
const selectedRoot = document.getElementById('moveModelRoot').value;
const apiClient = this._getApiClient();
const config = apiClient.apiConfig.config;
if (!selectedRoot) {
showToast('toast.models.pleaseSelectRoot', { type: config.displayName.toLowerCase() }, 'error');
return;
@@ -180,7 +251,7 @@ class MoveManager {
// Get selected folder path from folder tree manager
const targetFolder = this.folderTreeManager.getSelectedPath();
let targetPath = selectedRoot;
if (targetFolder) {
targetPath = `${targetPath}/${targetFolder}`;
@@ -189,7 +260,7 @@ class MoveManager {
try {
if (this.bulkFilePaths) {
// Bulk move mode
const results = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath);
const results = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath);
// Update virtual scroller if in active folder view
const pageState = getCurrentPageState();
@@ -206,7 +277,7 @@ class MoveManager {
if (result.success && result.new_file_path !== result.original_file_path) {
const newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1);
const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.'));
state.virtualScroller.updateSingleItem(result.original_file_path, {
file_path: result.new_file_path,
file_name: baseFileName
@@ -216,7 +287,7 @@ class MoveManager {
}
} else {
// Single move mode
const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath);
const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath);
const pageState = getCurrentPageState();
if (result && result.new_file_path) {
@@ -226,7 +297,7 @@ class MoveManager {
// Update both file_path and file_name if they changed
const newFileName = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1);
const baseFileName = newFileName.substring(0, newFileName.lastIndexOf('.'));
state.virtualScroller.updateSingleItem(this.currentFilePath, {
file_path: result.new_file_path,
file_name: baseFileName
@@ -239,7 +310,7 @@ class MoveManager {
sidebarManager.refresh();
modalManager.closeModal('moveModal');
// If we were in bulk mode, exit it after successful move
if (this.bulkFilePaths && state.bulkMode) {
bulkManager.toggleBulkMode();