mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
Backend fixes: - Add missing API route for /api/lm/recipes/batch-import/progress (GET) - Add missing API route for /api/lm/recipes/batch-import/directory (POST) - Add missing API route for /api/lm/recipes/browse-directory (POST) - Register WebSocket endpoint for batch import progress - Fix skip_no_metadata default value (True -> False) to allow no-LoRA imports - Add items array to BatchImportProgress.to_dict() for detailed results Frontend implementation: - Create BatchImportManager.js with complete batch import workflow - Add directory browser UI for selecting folders - Add batch import modal with URL list and directory input modes - Implement real-time progress tracking (WebSocket + HTTP polling) - Add results summary with success/failed/skipped statistics - Add expandable details view showing individual item status - Auto-refresh recipe list after import completion UI improvements: - Add spinner animation for importing status - Simplify results summary UI to match progress stats styling - Fix current item text alignment - Fix dark theme styling for directory browser button - Fix batch import button styling consistency Translations: - Add batch import related i18n keys to all locale files - Run sync_translation_keys.py to sync all translations Fixes: - Batch import now allows images without LoRAs (matches single import behavior) - Progress endpoint now returns complete items array with status details - Results view correctly displays skipped items with error messages
441 lines
16 KiB
JavaScript
441 lines
16 KiB
JavaScript
export class ModalManager {
|
|
constructor() {
|
|
this.modals = new Map();
|
|
this.scrollPosition = 0;
|
|
this.currentOpenModal = null; // Track currently open modal
|
|
this.mouseDownOnBackground = false; // Track if mousedown happened on modal background
|
|
}
|
|
|
|
initialize() {
|
|
if (this.initialized) return;
|
|
|
|
this.boundHandleEscape = this.handleEscape.bind(this);
|
|
|
|
// Register all modals - only if they exist in the current page
|
|
const modelModal = document.getElementById('modelModal');
|
|
if (modelModal) {
|
|
this.registerModal('modelModal', {
|
|
element: modelModal,
|
|
onClose: () => {
|
|
this.getModal('modelModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add checkpointDownloadModal registration
|
|
const checkpointDownloadModal = document.getElementById('checkpointDownloadModal');
|
|
if (checkpointDownloadModal) {
|
|
this.registerModal('checkpointDownloadModal', {
|
|
element: checkpointDownloadModal,
|
|
onClose: () => {
|
|
this.getModal('checkpointDownloadModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
if (deleteModal) {
|
|
this.registerModal('deleteModal', {
|
|
element: deleteModal,
|
|
onClose: () => {
|
|
this.getModal('deleteModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add excludeModal registration
|
|
const excludeModal = document.getElementById('excludeModal');
|
|
if (excludeModal) {
|
|
this.registerModal('excludeModal', {
|
|
element: excludeModal,
|
|
onClose: () => {
|
|
this.getModal('excludeModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add downloadModal registration
|
|
const downloadModal = document.getElementById('downloadModal');
|
|
if (downloadModal) {
|
|
this.registerModal('downloadModal', {
|
|
element: downloadModal,
|
|
onClose: () => {
|
|
this.getModal('downloadModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add settingsModal registration
|
|
const settingsModal = document.getElementById('settingsModal');
|
|
if (settingsModal) {
|
|
this.registerModal('settingsModal', {
|
|
element: settingsModal,
|
|
onClose: () => {
|
|
this.getModal('settingsModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add moveModal registration
|
|
const moveModal = document.getElementById('moveModal');
|
|
if (moveModal) {
|
|
this.registerModal('moveModal', {
|
|
element: moveModal,
|
|
onClose: () => {
|
|
this.getModal('moveModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add supportModal registration
|
|
const supportModal = document.getElementById('supportModal');
|
|
if (supportModal) {
|
|
this.registerModal('supportModal', {
|
|
element: supportModal,
|
|
onClose: () => {
|
|
this.getModal('supportModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add updateModal registration
|
|
const updateModal = document.getElementById('updateModal');
|
|
if (updateModal) {
|
|
this.registerModal('updateModal', {
|
|
element: updateModal,
|
|
onClose: () => {
|
|
this.getModal('updateModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add importModal registration
|
|
const importModal = document.getElementById('importModal');
|
|
if (importModal) {
|
|
this.registerModal('importModal', {
|
|
element: importModal,
|
|
onClose: () => {
|
|
this.getModal('importModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add batchImportModal registration
|
|
const batchImportModal = document.getElementById('batchImportModal');
|
|
if (batchImportModal) {
|
|
this.registerModal('batchImportModal', {
|
|
element: batchImportModal,
|
|
onClose: () => {
|
|
this.getModal('batchImportModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add recipeModal registration
|
|
const recipeModal = document.getElementById('recipeModal');
|
|
if (recipeModal) {
|
|
this.registerModal('recipeModal', {
|
|
element: recipeModal,
|
|
onClose: () => {
|
|
// Stop any playing video
|
|
const video = recipeModal.querySelector('video');
|
|
if (video) {
|
|
video.pause();
|
|
video.currentTime = 0;
|
|
}
|
|
this.getModal('recipeModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add duplicateDeleteModal registration
|
|
const duplicateDeleteModal = document.getElementById('duplicateDeleteModal');
|
|
if (duplicateDeleteModal) {
|
|
this.registerModal('duplicateDeleteModal', {
|
|
element: duplicateDeleteModal,
|
|
onClose: () => {
|
|
this.getModal('duplicateDeleteModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add modelDuplicateDeleteModal registration
|
|
const modelDuplicateDeleteModal = document.getElementById('modelDuplicateDeleteModal');
|
|
if (modelDuplicateDeleteModal) {
|
|
this.registerModal('modelDuplicateDeleteModal', {
|
|
element: modelDuplicateDeleteModal,
|
|
onClose: () => {
|
|
this.getModal('modelDuplicateDeleteModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add clearCacheModal registration
|
|
const clearCacheModal = document.getElementById('clearCacheModal');
|
|
if (clearCacheModal) {
|
|
this.registerModal('clearCacheModal', {
|
|
element: clearCacheModal,
|
|
onClose: () => {
|
|
this.getModal('clearCacheModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add bulkDeleteModal registration
|
|
const bulkDeleteModal = document.getElementById('bulkDeleteModal');
|
|
if (bulkDeleteModal) {
|
|
this.registerModal('bulkDeleteModal', {
|
|
element: bulkDeleteModal,
|
|
onClose: () => {
|
|
this.getModal('bulkDeleteModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add checkUpdatesConfirmModal registration
|
|
const checkUpdatesConfirmModal = document.getElementById('checkUpdatesConfirmModal');
|
|
if (checkUpdatesConfirmModal) {
|
|
this.registerModal('checkUpdatesConfirmModal', {
|
|
element: checkUpdatesConfirmModal,
|
|
onClose: () => {
|
|
this.getModal('checkUpdatesConfirmModal').element.classList.remove('show');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add helpModal registration
|
|
const helpModal = document.getElementById('helpModal');
|
|
if (helpModal) {
|
|
this.registerModal('helpModal', {
|
|
element: helpModal,
|
|
onClose: () => {
|
|
this.getModal('helpModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add relinkCivitaiModal registration
|
|
const relinkCivitaiModal = document.getElementById('relinkCivitaiModal');
|
|
if (relinkCivitaiModal) {
|
|
this.registerModal('relinkCivitaiModal', {
|
|
element: relinkCivitaiModal,
|
|
onClose: () => {
|
|
this.getModal('relinkCivitaiModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add exampleAccessModal registration
|
|
const exampleAccessModal = document.getElementById('exampleAccessModal');
|
|
if (exampleAccessModal) {
|
|
this.registerModal('exampleAccessModal', {
|
|
element: exampleAccessModal,
|
|
onClose: () => {
|
|
this.getModal('exampleAccessModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Add bulkAddTagsModal registration
|
|
const bulkAddTagsModal = document.getElementById('bulkAddTagsModal');
|
|
if (bulkAddTagsModal) {
|
|
this.registerModal('bulkAddTagsModal', {
|
|
element: bulkAddTagsModal,
|
|
onClose: () => {
|
|
this.getModal('bulkAddTagsModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
// Register bulkBaseModelModal
|
|
const bulkBaseModelModal = document.getElementById('bulkBaseModelModal');
|
|
if (bulkBaseModelModal) {
|
|
this.registerModal('bulkBaseModelModal', {
|
|
element: bulkBaseModelModal,
|
|
onClose: () => {
|
|
this.getModal('bulkBaseModelModal').element.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
},
|
|
closeOnOutsideClick: true
|
|
});
|
|
}
|
|
|
|
document.addEventListener('keydown', this.boundHandleEscape);
|
|
this.initialized = true;
|
|
}
|
|
|
|
registerModal(id, config) {
|
|
this.modals.set(id, {
|
|
element: config.element,
|
|
onClose: config.onClose,
|
|
isOpen: false
|
|
});
|
|
|
|
// Add click outside handler if specified in config
|
|
if (config.closeOnOutsideClick) {
|
|
// Track mousedown on modal background
|
|
config.element.addEventListener('mousedown', (e) => {
|
|
if (e.target === config.element) {
|
|
this.mouseDownOnBackground = true;
|
|
} else {
|
|
this.mouseDownOnBackground = false;
|
|
}
|
|
});
|
|
|
|
// Only close if mouseup is also on the background
|
|
config.element.addEventListener('mouseup', (e) => {
|
|
if (e.target === config.element && this.mouseDownOnBackground) {
|
|
this.closeModal(id);
|
|
}
|
|
// Reset flag regardless of target
|
|
this.mouseDownOnBackground = false;
|
|
});
|
|
|
|
// Cancel the flag if mouse leaves the document entirely
|
|
document.addEventListener('mouseleave', () => {
|
|
this.mouseDownOnBackground = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
getModal(id) {
|
|
return this.modals.get(id);
|
|
}
|
|
|
|
// Check if any modal is currently open
|
|
isAnyModalOpen() {
|
|
for (const [id, modal] of this.modals) {
|
|
if (modal.isOpen) {
|
|
return id;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
showModal(id, content = null, onCloseCallback = null, cleanupCallback = null) {
|
|
const modal = this.getModal(id);
|
|
if (!modal) return;
|
|
|
|
// Close any open modal before showing the new one
|
|
const openModalId = this.isAnyModalOpen();
|
|
if (openModalId && openModalId !== id) {
|
|
this.closeModal(openModalId);
|
|
}
|
|
|
|
if (content) {
|
|
modal.element.innerHTML = content;
|
|
}
|
|
|
|
// Store callback
|
|
modal.onCloseCallback = onCloseCallback;
|
|
modal.cleanupCallback = cleanupCallback;
|
|
|
|
// Store current scroll position before showing modal
|
|
this.scrollPosition = window.scrollY;
|
|
|
|
if (
|
|
id === "deleteModal" ||
|
|
id === "excludeModal" ||
|
|
id === "duplicateDeleteModal" ||
|
|
id === "modelDuplicateDeleteModal" ||
|
|
id === "clearCacheModal" ||
|
|
id === "bulkDeleteModal" ||
|
|
id === "checkUpdatesConfirmModal"
|
|
) {
|
|
modal.element.classList.add("show");
|
|
} else {
|
|
modal.element.style.display = "block";
|
|
}
|
|
|
|
modal.isOpen = true;
|
|
this.currentOpenModal = id; // Update currently open modal
|
|
document.body.style.top = `-${this.scrollPosition}px`;
|
|
document.body.classList.add('modal-open');
|
|
}
|
|
|
|
closeModal(id) {
|
|
const modal = this.getModal(id);
|
|
if (!modal) return;
|
|
|
|
modal.onClose();
|
|
modal.isOpen = false;
|
|
|
|
// Clear current open modal if this is the one being closed
|
|
if (this.currentOpenModal === id) {
|
|
this.currentOpenModal = null;
|
|
}
|
|
|
|
// Remove fixed positioning and restore scroll position
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.top = '';
|
|
window.scrollTo(0, this.scrollPosition);
|
|
|
|
// Execute onClose callback if exists
|
|
if (modal.onCloseCallback) {
|
|
modal.onCloseCallback();
|
|
modal.onCloseCallback = null;
|
|
}
|
|
|
|
if (modal.cleanupCallback) {
|
|
modal.cleanupCallback();
|
|
modal.cleanupCallback = null;
|
|
}
|
|
}
|
|
|
|
handleEscape(e) {
|
|
if (e.key === 'Escape') {
|
|
// Close the current open modal if it exists
|
|
if (this.currentOpenModal) {
|
|
this.closeModal(this.currentOpenModal);
|
|
}
|
|
}
|
|
}
|
|
|
|
toggleModal(id, content = null, onCloseCallback = null) {
|
|
const modal = this.getModal(id);
|
|
if (!modal) return;
|
|
|
|
// If this modal is already open, close it
|
|
if (modal.isOpen) {
|
|
this.closeModal(id);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, show the modal
|
|
this.showModal(id, content, onCloseCallback);
|
|
}
|
|
}
|
|
|
|
// Create and export a singleton instance
|
|
export const modalManager = new ModalManager(); |