mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
359 lines
14 KiB
JavaScript
359 lines
14 KiB
JavaScript
import { showToast } from '../utils/uiHelpers.js';
|
|
import { state, getCurrentPageState } from '../state/index.js';
|
|
import { modalManager } from './ModalManager.js';
|
|
import { bulkManager } from './BulkManager.js';
|
|
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() {
|
|
this.currentFilePath = null;
|
|
this.bulkFilePaths = null;
|
|
this.folderTreeManager = new FolderTreeManager();
|
|
this.initialized = false;
|
|
this.recipeApiClient = null;
|
|
this.useDefaultPath = false;
|
|
this.modelRoots = [];
|
|
|
|
// Bind methods
|
|
this.updateTargetPath = this.updateTargetPath.bind(this);
|
|
this.handleToggleDefaultPath = this.handleToggleDefaultPath.bind(this);
|
|
}
|
|
|
|
_getApiClient(modelType = null) {
|
|
if (state.currentPageType === 'recipes') {
|
|
if (!this.recipeApiClient) {
|
|
this.recipeApiClient = new RecipeSidebarApiClient();
|
|
}
|
|
return this.recipeApiClient;
|
|
}
|
|
return getModelApiClient(modelType);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async showMoveModal(filePath, modelType = null) {
|
|
// 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);
|
|
if (selectedPaths.length === 0) {
|
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
|
return;
|
|
}
|
|
this.bulkFilePaths = selectedPaths;
|
|
document.getElementById('moveModalTitle').textContent = `Move ${selectedPaths.length} ${modelConfig.displayName}s`;
|
|
} else {
|
|
// Single file mode
|
|
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) {
|
|
folderPathInput.value = '';
|
|
}
|
|
|
|
// Reset folder tree selection
|
|
if (this.folderTreeManager) {
|
|
this.folderTreeManager.clearSelection();
|
|
}
|
|
|
|
try {
|
|
// Fetch model roots
|
|
const modelRootSelect = document.getElementById('moveModelRoot');
|
|
let rootsData;
|
|
if (modelType) {
|
|
rootsData = await apiClient.fetchModelRoots(modelType);
|
|
} 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 =>
|
|
`<option value="${root}">${root}</option>`
|
|
).join('');
|
|
|
|
// Set default root if available
|
|
const settingsKey = `default_${currentPageType.slice(0, -1)}_root`;
|
|
const defaultRoot = state.global.settings[settingsKey];
|
|
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
|
modelRootSelect.value = defaultRoot;
|
|
}
|
|
|
|
// Store roots for path calculations
|
|
this.modelRoots = rootsData.roots || [];
|
|
|
|
// Initialize event listeners
|
|
this.initializeEventListeners();
|
|
|
|
// Setup folder tree manager
|
|
this.folderTreeManager.init({
|
|
onPathChange: (path) => {
|
|
this.updateTargetPath();
|
|
},
|
|
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
|
|
if (this.folderTreeManager) {
|
|
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);
|
|
} else {
|
|
console.error('Failed to fetch folder tree:', treeData.error);
|
|
showToast('toast.import.folderTreeFailed', {}, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error initializing folder tree:', error);
|
|
showToast('toast.import.folderTreeError', {}, 'error');
|
|
}
|
|
}
|
|
|
|
updateTargetPath() {
|
|
const pathDisplay = document.getElementById('moveTargetPathDisplay');
|
|
const modelRoot = document.getElementById('moveModelRoot').value;
|
|
const apiClient = this._getApiClient();
|
|
const config = apiClient.apiConfig.config;
|
|
|
|
let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: config.displayName });
|
|
|
|
if (modelRoot) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
|
}
|
|
|
|
/**
|
|
* Get relative folder path from absolute file path
|
|
* @param {string} absolutePath
|
|
* @returns {string} Relative folder path using forward slashes
|
|
*/
|
|
_getRelativeFolder(absolutePath) {
|
|
if (!absolutePath) return '';
|
|
const normalizedPath = absolutePath.replace(/\\/g, '/');
|
|
|
|
for (const root of this.modelRoots || []) {
|
|
const normalizedRoot = root.replace(/\\/g, '/');
|
|
if (normalizedPath.startsWith(normalizedRoot)) {
|
|
let relative = normalizedPath.substring(normalizedRoot.length);
|
|
if (relative.startsWith('/')) relative = relative.substring(1);
|
|
|
|
// Get the directory part
|
|
const lastSlash = relative.lastIndexOf('/');
|
|
if (lastSlash === -1) return ''; // In the root itself
|
|
return relative.substring(0, lastSlash);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Check if a model should be visible based on its relative folder and current page state
|
|
* @param {string} relativeFolder
|
|
* @param {Object} pageState
|
|
* @returns {boolean}
|
|
*/
|
|
_isModelVisible(relativeFolder, pageState) {
|
|
if (!pageState) return true;
|
|
// If no folder filter is active, check search recursive option
|
|
if (pageState.activeFolder === null) return true;
|
|
|
|
const activeFolder = pageState.activeFolder || '';
|
|
const recursive = pageState.searchOptions?.recursive ?? true;
|
|
|
|
const normalizedActive = activeFolder.replace(/\\/g, '/').replace(/\/$/, '');
|
|
const normalizedRelative = relativeFolder.replace(/\\/g, '/').replace(/\/$/, '');
|
|
|
|
if (recursive) {
|
|
// Visible if it's in activeFolder or any subfolder
|
|
// Special case for root: if activeFolder is empty, everything is visible in recursive mode
|
|
if (normalizedActive === '') return true;
|
|
|
|
return normalizedRelative === normalizedActive ||
|
|
normalizedRelative.startsWith(normalizedActive + '/');
|
|
} else {
|
|
// Only visible if it's exactly in activeFolder
|
|
return normalizedRelative === normalizedActive;
|
|
}
|
|
}
|
|
|
|
async moveModel() {
|
|
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;
|
|
}
|
|
|
|
// Get selected folder path from folder tree manager
|
|
const targetFolder = this.useDefaultPath ? '' : this.folderTreeManager.getSelectedPath();
|
|
|
|
let targetPath = selectedRoot;
|
|
if (targetFolder) {
|
|
targetPath = `${targetPath}/${targetFolder}`;
|
|
}
|
|
|
|
try {
|
|
if (this.bulkFilePaths) {
|
|
// Bulk move mode
|
|
await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath);
|
|
|
|
// Deselect moving items
|
|
this.bulkFilePaths.forEach(path => bulkManager.deselectItem(path));
|
|
} else {
|
|
// Single move mode
|
|
await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath);
|
|
|
|
// Deselect moving item
|
|
bulkManager.deselectItem(this.currentFilePath);
|
|
}
|
|
|
|
// Refresh UI by reloading the current page, same as drag-and-drop behavior
|
|
// This ensures all metadata (like preview URLs) are correctly formatted by the backend
|
|
if (sidebarManager.pageControls && typeof sidebarManager.pageControls.resetAndReload === 'function') {
|
|
await sidebarManager.pageControls.resetAndReload(true);
|
|
} else if (sidebarManager.lastPageControls && typeof sidebarManager.lastPageControls.resetAndReload === 'function') {
|
|
await sidebarManager.lastPageControls.resetAndReload(true);
|
|
}
|
|
|
|
// Refresh folder tree in sidebar
|
|
await sidebarManager.refresh();
|
|
|
|
modalManager.closeModal('moveModal');
|
|
|
|
} catch (error) {
|
|
console.error('Error moving model(s):', error);
|
|
showToast('toast.models.moveFailed', { message: error.message }, 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
export const moveManager = new MoveManager();
|