From 7f205cdcc828aadcaca6daba2d06ce243bbda932 Mon Sep 17 00:00:00 2001
From: Will Miao <13051207myq@gmail.com>
Date: Fri, 25 Jul 2025 17:35:06 +0800
Subject: [PATCH] refactor: unify model download system across all model types
- Add download-related methods to baseModelApi.js for fetching versions, roots, folders, and downloading models
- Replace separate download managers with a unified DownloadManager.js supporting all model types
- Create a single download_modals.html template that adapts to model type (LoRA, checkpoint, etc.)
- Remove old download modals from lora_modals.html and checkpoint_modals.html
- Update apiConfig.js to include civitaiVersions endpoints for each model type
- Centralize event handler binding in DownloadManager.js (no more inline HTML handlers)
- Modal UI and logic now auto-adapt to the current model type, making future extension easier
---
static/js/api/apiConfig.js | 5 +-
static/js/api/baseModelApi.js | 80 +++
static/js/checkpoints.js | 4 -
.../ContextMenu/CheckpointContextMenu.js | 7 +-
.../controls/CheckpointsControls.js | 7 +-
.../js/components/controls/LorasControls.js | 7 +-
static/js/loras.js | 5 -
.../js/managers/CheckpointDownloadManager.js | 463 ------------------
static/js/managers/DownloadManager.js | 259 +++++-----
templates/checkpoints.html | 1 -
templates/components/checkpoint_modals.html | 73 ---
templates/components/lora_modals.html | 102 ----
templates/components/modals.html | 7 +-
.../components/modals/download_modal.html | 62 +++
templates/components/modals/move_modal.html | 36 ++
templates/loras.html | 4 -
16 files changed, 337 insertions(+), 785 deletions(-)
delete mode 100644 static/js/managers/CheckpointDownloadManager.js
delete mode 100644 templates/components/checkpoint_modals.html
delete mode 100644 templates/components/lora_modals.html
create mode 100644 templates/components/modals/download_modal.html
create mode 100644 templates/components/modals/move_modal.html
diff --git a/static/js/api/apiConfig.js b/static/js/api/apiConfig.js
index 6f02033f..53b3f842 100644
--- a/static/js/api/apiConfig.js
+++ b/static/js/api/apiConfig.js
@@ -103,13 +103,12 @@ export const MODEL_SPECIFIC_ENDPOINTS = {
moveBulk: `/api/${MODEL_TYPES.LORA}/move_models_bulk`,
getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`,
civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`,
- civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`
+ civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`,
},
[MODEL_TYPES.CHECKPOINT]: {
- info: `/api/${MODEL_TYPES.CHECKPOINT}/info`
+ info: `/api/${MODEL_TYPES.CHECKPOINT}/info`,
},
[MODEL_TYPES.EMBEDDING]: {
- // Future embedding-specific endpoints
}
};
diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js
index 692a83db..042871cd 100644
--- a/static/js/api/baseModelApi.js
+++ b/static/js/api/baseModelApi.js
@@ -581,6 +581,86 @@ class ModelApiClient {
return successFilePaths;
}
+ /**
+ * Fetch Civitai model versions
+ */
+ async fetchCivitaiVersions(modelId) {
+ try {
+ const response = await fetch(`${this.apiConfig.endpoints.civitaiVersions}/${modelId}`);
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ if (errorData && errorData.error && errorData.error.includes('Model type mismatch')) {
+ throw new Error(`This model is not a ${this.apiConfig.config.displayName}. Please switch to the appropriate page to download this model type.`);
+ }
+ throw new Error('Failed to fetch model versions');
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching Civitai versions:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetch model roots
+ */
+ async fetchModelRoots() {
+ try {
+ const response = await fetch(this.apiConfig.endpoints.roots);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch ${this.apiConfig.config.displayName} roots`);
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching model roots:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Fetch model folders
+ */
+ async fetchModelFolders() {
+ try {
+ const response = await fetch(this.apiConfig.endpoints.folders);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch ${this.apiConfig.config.displayName} folders`);
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching model folders:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Download a model
+ */
+ async downloadModel(modelId, versionId, modelRoot, relativePath, downloadId) {
+ try {
+ const response = await fetch(DOWNLOAD_ENDPOINTS.download, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model_id: modelId,
+ model_version_id: versionId,
+ model_root: modelRoot,
+ relative_path: relativePath,
+ download_id: downloadId
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(await response.text());
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error downloading model:', error);
+ throw error;
+ }
+ }
+
/**
* Build query parameters for API requests
*/
diff --git a/static/js/checkpoints.js b/static/js/checkpoints.js
index 69ab6224..cbc02ace 100644
--- a/static/js/checkpoints.js
+++ b/static/js/checkpoints.js
@@ -2,7 +2,6 @@ import { appCore } from './core.js';
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
import { createPageControls } from './components/controls/index.js';
import { loadMoreCheckpoints } from './api/checkpointApi.js';
-import { CheckpointDownloadManager } from './managers/CheckpointDownloadManager.js';
import { CheckpointContextMenu } from './components/ContextMenu/index.js';
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
import { MODEL_TYPES } from './api/apiConfig.js';
@@ -13,9 +12,6 @@ class CheckpointsPageManager {
// Initialize page controls
this.pageControls = createPageControls(MODEL_TYPES.CHECKPOINT);
- // Initialize checkpoint download manager
- window.checkpointDownloadManager = new CheckpointDownloadManager();
-
// Initialize the ModelDuplicatesManager
this.duplicatesManager = new ModelDuplicatesManager(this, MODEL_TYPES.CHECKPOINT);
diff --git a/static/js/components/ContextMenu/CheckpointContextMenu.js b/static/js/components/ContextMenu/CheckpointContextMenu.js
index 8e589b51..b7be4b9f 100644
--- a/static/js/components/ContextMenu/CheckpointContextMenu.js
+++ b/static/js/components/ContextMenu/CheckpointContextMenu.js
@@ -3,7 +3,7 @@ import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
import { resetAndReload } from '../../api/checkpointApi.js';
import { getModelApiClient } from '../../api/baseModelApi.js';
import { showToast } from '../../utils/uiHelpers.js';
-import { showExcludeModal } from '../../utils/modalUtils.js';
+import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
export class CheckpointContextMenu extends BaseContextMenu {
constructor() {
@@ -42,10 +42,7 @@ export class CheckpointContextMenu extends BaseContextMenu {
apiClient.replaceModelPreview(this.currentCard.dataset.filepath);
break;
case 'delete':
- // Delete checkpoint
- if (this.currentCard.querySelector('.fa-trash')) {
- this.currentCard.querySelector('.fa-trash').click();
- }
+ showDeleteModal(this.currentCard.dataset.filepath);
break;
case 'copyname':
// Copy checkpoint name
diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js
index f5af38a8..6528a8a5 100644
--- a/static/js/components/controls/CheckpointsControls.js
+++ b/static/js/components/controls/CheckpointsControls.js
@@ -2,7 +2,7 @@
import { PageControls } from './PageControls.js';
import { loadMoreCheckpoints, resetAndReload, refreshCheckpoints, fetchCivitai } from '../../api/checkpointApi.js';
import { showToast } from '../../utils/uiHelpers.js';
-import { CheckpointDownloadManager } from '../../managers/CheckpointDownloadManager.js';
+import { downloadManager } from '../../managers/DownloadManager.js';
/**
* CheckpointsControls class - Extends PageControls for Checkpoint-specific functionality
@@ -12,9 +12,6 @@ export class CheckpointsControls extends PageControls {
// Initialize with 'checkpoints' page type
super('checkpoints');
- // Initialize checkpoint download manager
- this.downloadManager = new CheckpointDownloadManager();
-
// Register API methods specific to the Checkpoints page
this.registerCheckpointsAPI();
}
@@ -44,7 +41,7 @@ export class CheckpointsControls extends PageControls {
// Add show download modal functionality
showDownloadModal: () => {
- this.downloadManager.showDownloadModal();
+ downloadManager.showDownloadModal();
},
// No clearCustomFilter implementation is needed for checkpoints
diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js
index 22f768d4..dee5e128 100644
--- a/static/js/components/controls/LorasControls.js
+++ b/static/js/components/controls/LorasControls.js
@@ -3,6 +3,7 @@ import { PageControls } from './PageControls.js';
import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js';
import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
import { createAlphabetBar } from '../alphabet/index.js';
+import { downloadManager } from '../../managers/DownloadManager.js';
/**
* LorasControls class - Extends PageControls for LoRA-specific functionality
@@ -46,11 +47,7 @@ export class LorasControls extends PageControls {
},
showDownloadModal: () => {
- if (window.downloadManager) {
- window.downloadManager.showDownloadModal();
- } else {
- console.error('Download manager not available');
- }
+ downloadManager.showDownloadModal();
},
toggleBulkMode: () => {
diff --git a/static/js/loras.js b/static/js/loras.js
index c5e2f77a..537ed628 100644
--- a/static/js/loras.js
+++ b/static/js/loras.js
@@ -3,7 +3,6 @@ import { state } from './state/index.js';
import { loadMoreLoras } from './api/loraApi.js';
import { updateCardsForBulkMode } from './components/LoraCard.js';
import { bulkManager } from './managers/BulkManager.js';
-import { DownloadManager } from './managers/DownloadManager.js';
import { moveManager } from './managers/MoveManager.js';
import { LoraContextMenu } from './components/ContextMenu/index.js';
import { createPageControls } from './components/controls/index.js';
@@ -17,9 +16,6 @@ class LoraPageManager {
state.bulkMode = false;
state.selectedLoras = new Set();
- // Initialize managers
- this.downloadManager = new DownloadManager();
-
// Initialize page controls
this.pageControls = createPageControls('loras');
@@ -39,7 +35,6 @@ class LoraPageManager {
window.closeDeleteModal = closeDeleteModal;
window.confirmExclude = confirmExclude;
window.closeExcludeModal = closeExcludeModal;
- window.downloadManager = this.downloadManager;
window.moveManager = moveManager;
// Bulk operations
diff --git a/static/js/managers/CheckpointDownloadManager.js b/static/js/managers/CheckpointDownloadManager.js
deleted file mode 100644
index 7545e617..00000000
--- a/static/js/managers/CheckpointDownloadManager.js
+++ /dev/null
@@ -1,463 +0,0 @@
-import { modalManager } from './ModalManager.js';
-import { showToast } from '../utils/uiHelpers.js';
-import { LoadingManager } from './LoadingManager.js';
-import { state } from '../state/index.js';
-import { resetAndReload } from '../api/checkpointApi.js';
-import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
-
-export class CheckpointDownloadManager {
- constructor() {
- this.currentVersion = null;
- this.versions = [];
- this.modelInfo = null;
- this.modelVersionId = null;
-
- this.initialized = false;
- this.selectedFolder = '';
-
- this.loadingManager = new LoadingManager();
- this.folderClickHandler = null;
- this.updateTargetPath = this.updateTargetPath.bind(this);
- }
-
- showDownloadModal() {
- console.log('Showing checkpoint download modal...');
- if (!this.initialized) {
- const modal = document.getElementById('checkpointDownloadModal');
- if (!modal) {
- console.error('Checkpoint download modal element not found');
- return;
- }
- this.initialized = true;
- }
-
- modalManager.showModal('checkpointDownloadModal', null, () => {
- // Cleanup handler when modal closes
- this.cleanupFolderBrowser();
- });
- this.resetSteps();
-
- // Auto-focus on the URL input
- setTimeout(() => {
- const urlInput = document.getElementById('checkpointUrl');
- if (urlInput) {
- urlInput.focus();
- }
- }, 100); // Small delay to ensure the modal is fully displayed
- }
-
- resetSteps() {
- document.querySelectorAll('#checkpointDownloadModal .download-step').forEach(step => step.style.display = 'none');
- document.getElementById('cpUrlStep').style.display = 'block';
- document.getElementById('checkpointUrl').value = '';
- document.getElementById('cpUrlError').textContent = '';
-
- // Clear new folder input
- const newFolderInput = document.getElementById('cpNewFolder');
- if (newFolderInput) {
- newFolderInput.value = '';
- }
-
- this.currentVersion = null;
- this.versions = [];
- this.modelInfo = null;
- this.modelId = null;
- this.modelVersionId = null;
-
- // Clear selected folder and remove selection from UI
- this.selectedFolder = '';
- const folderBrowser = document.getElementById('cpFolderBrowser');
- if (folderBrowser) {
- folderBrowser.querySelectorAll('.folder-item').forEach(f =>
- f.classList.remove('selected'));
- }
- }
-
- async validateAndFetchVersions() {
- const url = document.getElementById('checkpointUrl').value.trim();
- const errorElement = document.getElementById('cpUrlError');
-
- try {
- this.loadingManager.showSimpleLoading('Fetching model versions...');
-
- this.modelId = this.extractModelId(url);
- if (!this.modelId) {
- throw new Error('Invalid Civitai URL format');
- }
-
- const response = await fetch(`/api/checkpoints/civitai/versions/${this.modelId}`);
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- if (errorData && errorData.error && errorData.error.includes('Model type mismatch')) {
- throw new Error('This model is not a Checkpoint. Please switch to the LoRAs page to download LoRA models.');
- }
- throw new Error('Failed to fetch model versions');
- }
-
- this.versions = await response.json();
- if (!this.versions.length) {
- throw new Error('No versions available for this model');
- }
-
- // If we have a version ID from URL, pre-select it
- if (this.modelVersionId) {
- this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
- }
-
- this.showVersionStep();
- } catch (error) {
- errorElement.textContent = error.message;
- } finally {
- this.loadingManager.hide();
- }
- }
-
- extractModelId(url) {
- const modelMatch = url.match(/civitai\.com\/models\/(\d+)/);
- const versionMatch = url.match(/modelVersionId=(\d+)/);
-
- if (modelMatch) {
- this.modelVersionId = versionMatch ? versionMatch[1] : null;
- return modelMatch[1];
- }
- return null;
- }
-
- showVersionStep() {
- document.getElementById('cpUrlStep').style.display = 'none';
- document.getElementById('cpVersionStep').style.display = 'block';
-
- const versionList = document.getElementById('cpVersionList');
- 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';
-
- // Use version-level size or fallback to first file
- const fileSize = version.modelSizeKB ?
- (version.modelSizeKB / 1024).toFixed(2) :
- (version.files[0]?.sizeKB / 1024).toFixed(2);
-
- // Use version-level existsLocally flag
- const existsLocally = version.existsLocally;
- const localPath = version.localPath;
-
- // Check if this is an early access version
- const isEarlyAccess = version.availability === 'EarlyAccess';
-
- // Create early access badge if needed
- let earlyAccessBadge = '';
- if (isEarlyAccess) {
- earlyAccessBadge = `
-
- Early Access
-
- `;
- }
-
- // Status badge for local models
- const localStatus = existsLocally ?
- `
-
In Library
-
${localPath || ''}
-
` : '';
-
- return `
-
-
-
-
-
-
-
- ${version.baseModel ? `
${version.baseModel}
` : ''}
- ${earlyAccessBadge}
-
-
- ${new Date(version.createdAt).toLocaleDateString()}
- ${fileSize} MB
-
-
-
- `;
- }).join('');
-
- // Auto-select the version if there's only one
- if (this.versions.length === 1 && !this.currentVersion) {
- this.selectVersion(this.versions[0].id.toString());
- }
-
- // Update Next button state based on initial selection
- this.updateNextButtonState();
- }
-
- selectVersion(versionId) {
- this.currentVersion = this.versions.find(v => v.id.toString() === versionId.toString());
- if (!this.currentVersion) return;
-
- document.querySelectorAll('#cpVersionList .version-item').forEach(item => {
- item.classList.toggle('selected', item.querySelector('h3').textContent === this.currentVersion.name);
- });
-
- // Update Next button state after selection
- this.updateNextButtonState();
- }
-
- updateNextButtonState() {
- const nextButton = document.querySelector('#cpVersionStep .primary-btn');
- if (!nextButton) return;
-
- const existsLocally = this.currentVersion?.existsLocally;
-
- if (existsLocally) {
- nextButton.disabled = true;
- nextButton.classList.add('disabled');
- nextButton.textContent = 'Already in Library';
- } else {
- nextButton.disabled = false;
- nextButton.classList.remove('disabled');
- nextButton.textContent = 'Next';
- }
- }
-
- async proceedToLocation() {
- if (!this.currentVersion) {
- showToast('Please select a version', 'error');
- return;
- }
-
- // Double-check if the version exists locally
- const existsLocally = this.currentVersion.existsLocally;
- if (existsLocally) {
- showToast('This version already exists in your library', 'info');
- return;
- }
-
- document.getElementById('cpVersionStep').style.display = 'none';
- document.getElementById('cpLocationStep').style.display = 'block';
-
- try {
- // Use checkpoint roots endpoint instead of lora roots
- const response = await fetch('/api/checkpoints/roots');
- if (!response.ok) {
- throw new Error('Failed to fetch checkpoint roots');
- }
-
- const data = await response.json();
- const checkpointRoot = document.getElementById('checkpointRoot');
- checkpointRoot.innerHTML = data.roots.map(root =>
- `${root} `
- ).join('');
-
- // Set default checkpoint root if available
- const defaultRoot = getStorageItem('settings', {}).default_checkpoint_root;
- if (defaultRoot && data.roots.includes(defaultRoot)) {
- checkpointRoot.value = defaultRoot;
- }
-
- // Initialize folder browser after loading roots
- this.initializeFolderBrowser();
- } catch (error) {
- showToast(error.message, 'error');
- }
- }
-
- backToUrl() {
- document.getElementById('cpVersionStep').style.display = 'none';
- document.getElementById('cpUrlStep').style.display = 'block';
- }
-
- backToVersions() {
- document.getElementById('cpLocationStep').style.display = 'none';
- document.getElementById('cpVersionStep').style.display = 'block';
- }
-
- async startDownload() {
- const checkpointRoot = document.getElementById('checkpointRoot').value;
- const newFolder = document.getElementById('cpNewFolder').value.trim();
-
- if (!checkpointRoot) {
- showToast('Please select a checkpoint root directory', 'error');
- return;
- }
-
- // Construct relative path
- let targetFolder = '';
- if (this.selectedFolder) {
- targetFolder = this.selectedFolder;
- }
- if (newFolder) {
- targetFolder = targetFolder ?
- `${targetFolder}/${newFolder}` : newFolder;
- }
-
- try {
- // Show enhanced loading with progress details
- const updateProgress = this.loadingManager.showDownloadProgress(1);
- updateProgress(0, 0, this.currentVersion.name);
-
- // Generate a unique ID for this download
- const downloadId = Date.now().toString();
-
- // Setup WebSocket for progress updates using download-specific endpoint
- const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
- const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
-
- ws.onmessage = (event) => {
- const data = JSON.parse(event.data);
-
- // Handle download ID confirmation
- if (data.type === 'download_id') {
- console.log(`Connected to checkpoint download progress with ID: ${data.download_id}`);
- return;
- }
-
- // Only process progress updates for our download
- if (data.status === 'progress' && data.download_id === downloadId) {
- // Update progress display with current progress
- updateProgress(data.progress, 0, this.currentVersion.name);
-
- // Add more detailed status messages based on progress
- if (data.progress < 3) {
- this.loadingManager.setStatus(`Preparing download...`);
- } else if (data.progress === 3) {
- this.loadingManager.setStatus(`Downloaded preview image`);
- } else if (data.progress > 3 && data.progress < 100) {
- this.loadingManager.setStatus(`Downloading checkpoint file`);
- } else {
- this.loadingManager.setStatus(`Finalizing download...`);
- }
- }
- };
-
- ws.onerror = (error) => {
- console.error('WebSocket error:', error);
- // Continue with download even if WebSocket fails
- };
-
- // Start download using checkpoint download endpoint with download ID
- const response = await fetch('/api/download-model', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- model_id: this.modelId,
- model_version_id: this.currentVersion.id,
- model_root: checkpointRoot,
- relative_path: targetFolder,
- download_id: downloadId
- })
- });
-
- if (!response.ok) {
- throw new Error(await response.text());
- }
-
- showToast('Download completed successfully', 'success');
- modalManager.closeModal('checkpointDownloadModal');
-
- // Update state specifically for the checkpoints page
- state.pages.checkpoints.activeFolder = targetFolder;
-
- // Save the active folder preference to storage
- setStorageItem('checkpoints_activeFolder', targetFolder);
-
- // Update UI to show the folder as selected
- document.querySelectorAll('.folder-tags .tag').forEach(tag => {
- const isActive = tag.dataset.folder === targetFolder;
- tag.classList.toggle('active', isActive);
- if (isActive && !tag.parentNode.classList.contains('collapsed')) {
- // Scroll the tag into view if folder tags are not collapsed
- tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- }
- });
-
- await resetAndReload(true); // Pass true to update folders
-
- } catch (error) {
- showToast(error.message, 'error');
- } finally {
- this.loadingManager.hide();
- }
- }
-
- initializeFolderBrowser() {
- const folderBrowser = document.getElementById('cpFolderBrowser');
- if (!folderBrowser) return;
-
- // Cleanup existing handler if any
- this.cleanupFolderBrowser();
-
- // Create new handler
- this.folderClickHandler = (event) => {
- const folderItem = event.target.closest('.folder-item');
- if (!folderItem) return;
-
- if (folderItem.classList.contains('selected')) {
- folderItem.classList.remove('selected');
- this.selectedFolder = '';
- } else {
- folderBrowser.querySelectorAll('.folder-item').forEach(f =>
- f.classList.remove('selected'));
- folderItem.classList.add('selected');
- this.selectedFolder = folderItem.dataset.folder;
- }
-
- // Update path display after folder selection
- this.updateTargetPath();
- };
-
- // Add the new handler
- folderBrowser.addEventListener('click', this.folderClickHandler);
-
- // Add event listeners for path updates
- const checkpointRoot = document.getElementById('checkpointRoot');
- const newFolder = document.getElementById('cpNewFolder');
-
- checkpointRoot.addEventListener('change', this.updateTargetPath);
- newFolder.addEventListener('input', this.updateTargetPath);
-
- // Update initial path
- this.updateTargetPath();
- }
-
- cleanupFolderBrowser() {
- if (this.folderClickHandler) {
- const folderBrowser = document.getElementById('cpFolderBrowser');
- if (folderBrowser) {
- folderBrowser.removeEventListener('click', this.folderClickHandler);
- this.folderClickHandler = null;
- }
- }
-
- // Remove path update listeners
- const checkpointRoot = document.getElementById('checkpointRoot');
- const newFolder = document.getElementById('cpNewFolder');
-
- if (checkpointRoot) checkpointRoot.removeEventListener('change', this.updateTargetPath);
- if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
- }
-
- updateTargetPath() {
- const pathDisplay = document.getElementById('cpTargetPathDisplay');
- const checkpointRoot = document.getElementById('checkpointRoot').value;
- const newFolder = document.getElementById('cpNewFolder').value.trim();
-
- let fullPath = checkpointRoot || 'Select a checkpoint root directory';
-
- if (checkpointRoot) {
- if (this.selectedFolder) {
- fullPath += '/' + this.selectedFolder;
- }
- if (newFolder) {
- fullPath += '/' + newFolder;
- }
- }
-
- pathDisplay.innerHTML = `${fullPath} `;
- }
-}
\ No newline at end of file
diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js
index f4f5b9ba..d5c6ab29 100644
--- a/static/js/managers/DownloadManager.js
+++ b/static/js/managers/DownloadManager.js
@@ -1,60 +1,111 @@
import { modalManager } from './ModalManager.js';
import { showToast } from '../utils/uiHelpers.js';
import { LoadingManager } from './LoadingManager.js';
-import { state } from '../state/index.js';
-import { resetAndReload } from '../api/loraApi.js';
-import { getStorageItem } from '../utils/storageHelpers.js';
+import { getModelApiClient } from '../api/baseModelApi.js';
+import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
+
export class DownloadManager {
constructor() {
this.currentVersion = null;
this.versions = [];
this.modelInfo = null;
- this.modelVersionId = null; // Add new property for initial version ID
+ this.modelVersionId = null;
+ this.modelId = null;
- // Add initialization check
this.initialized = false;
this.selectedFolder = '';
+ this.apiClient = null;
- // Add LoadingManager instance
this.loadingManager = new LoadingManager();
- this.folderClickHandler = null; // Add this line
+ 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);
+ this.handleStartDownload = this.startDownload.bind(this);
+ this.handleBackToUrl = this.backToUrl.bind(this);
+ this.handleBackToVersions = this.backToVersions.bind(this);
+ this.handleCloseModal = this.closeModal.bind(this);
}
showDownloadModal() {
- console.log('Showing download modal...'); // Add debug log
+ 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) {
- // Check if modal exists
const modal = document.getElementById('downloadModal');
if (!modal) {
- console.error('Download modal element not found');
+ console.error('Unified download modal element not found');
return;
}
+ this.initializeEventHandlers();
this.initialized = true;
}
+ // Update modal title and labels based on model type
+ this.updateModalLabels();
+
modalManager.showModal('downloadModal', null, () => {
- // Cleanup handler when modal closes
this.cleanupFolderBrowser();
});
this.resetSteps();
// Auto-focus on the URL input
setTimeout(() => {
- const urlInput = document.getElementById('loraUrl');
+ const urlInput = document.getElementById('modelUrl');
if (urlInput) {
urlInput.focus();
}
- }, 100); // Small delay to ensure the modal is fully displayed
+ }, 100);
+ }
+
+ initializeEventHandlers() {
+ // Button event handlers
+ document.getElementById('nextFromUrl').addEventListener('click', this.handleValidateAndFetchVersions);
+ document.getElementById('nextFromVersion').addEventListener('click', this.handleProceedToLocation);
+ document.getElementById('startDownloadBtn').addEventListener('click', this.handleStartDownload);
+ document.getElementById('backToUrlBtn').addEventListener('click', this.handleBackToUrl);
+ document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions);
+ document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal);
+ }
+
+ updateModalLabels() {
+ const config = this.apiClient.apiConfig.config;
+
+ // Update modal title
+ document.getElementById('downloadModalTitle').textContent = `Download ${config.displayName} from URL`;
+
+ // Update URL label
+ document.getElementById('modelUrlLabel').textContent = 'Civitai URL:';
+
+ // Update root selection label
+ document.getElementById('modelRootLabel').textContent = `Select ${config.displayName} Root:`;
+
+ // Update path preview labels
+ const pathLabels = document.querySelectorAll('.path-preview label');
+ pathLabels.forEach(label => {
+ if (label.textContent.includes('Location Preview')) {
+ label.textContent = 'Download Location Preview:';
+ }
+ });
+
+ // Update initial path text
+ const pathText = document.querySelector('#targetPathDisplay .path-text');
+ if (pathText) {
+ pathText.textContent = `Select a ${config.displayName} root directory`;
+ }
}
resetSteps() {
document.querySelectorAll('.download-step').forEach(step => step.style.display = 'none');
document.getElementById('urlStep').style.display = 'block';
- document.getElementById('loraUrl').value = '';
+ document.getElementById('modelUrl').value = '';
document.getElementById('urlError').textContent = '';
- // Clear new folder input
const newFolderInput = document.getElementById('newFolder');
if (newFolderInput) {
newFolderInput.value = '';
@@ -66,7 +117,6 @@ export class DownloadManager {
this.modelId = null;
this.modelVersionId = null;
- // Clear selected folder and remove selection from UI
this.selectedFolder = '';
const folderBrowser = document.getElementById('folderBrowser');
if (folderBrowser) {
@@ -76,7 +126,7 @@ export class DownloadManager {
}
async validateAndFetchVersions() {
- const url = document.getElementById('loraUrl').value.trim();
+ const url = document.getElementById('modelUrl').value.trim();
const errorElement = document.getElementById('urlError');
try {
@@ -87,16 +137,8 @@ export class DownloadManager {
throw new Error('Invalid Civitai URL format');
}
- const response = await fetch(`/api/loras/civitai/versions/${this.modelId}`);
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- if (errorData && errorData.error && errorData.error.includes('Model type mismatch')) {
- throw new Error('This model is not a LoRA. Please switch to the Checkpoints page to download checkpoint models.');
- }
- throw new Error('Failed to fetch model versions');
- }
+ this.versions = await this.apiClient.fetchCivitaiVersions(this.modelId);
- this.versions = await response.json();
if (!this.versions.length) {
throw new Error('No versions available for this model');
}
@@ -134,19 +176,14 @@ export class DownloadManager {
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
- // Use version-level size or fallback to first file
const fileSize = version.modelSizeKB ?
(version.modelSizeKB / 1024).toFixed(2) :
(version.files[0]?.sizeKB / 1024).toFixed(2);
- // Use version-level existsLocally flag
const existsLocally = version.existsLocally;
const localPath = version.localPath;
-
- // Check if this is an early access version
const isEarlyAccess = version.availability === 'EarlyAccess';
- // Create early access badge if needed
let earlyAccessBadge = '';
if (isEarlyAccess) {
earlyAccessBadge = `
@@ -156,7 +193,6 @@ export class DownloadManager {
`;
}
- // Status badge for local models
const localStatus = existsLocally ?
`
In Library
@@ -167,7 +203,7 @@ export class DownloadManager {
+ data-version-id="${version.id}">
@@ -189,12 +225,19 @@ export class DownloadManager {
`;
}).join('');
+ // Add click handlers for version selection
+ versionList.addEventListener('click', (event) => {
+ const versionItem = event.target.closest('.version-item');
+ if (versionItem) {
+ 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());
}
- // Update Next button state based on initial selection
this.updateNextButtonState();
}
@@ -202,23 +245,15 @@ export class DownloadManager {
this.currentVersion = this.versions.find(v => v.id.toString() === versionId.toString());
if (!this.currentVersion) return;
- // Remove the toast notification - it's redundant with the visual indicator
- // const existsLocally = this.currentVersion.files[0]?.existsLocally;
- // if (existsLocally) {
- // showToast('This version already exists in your library', 'info');
- // }
-
document.querySelectorAll('.version-item').forEach(item => {
- item.classList.toggle('selected', item.querySelector('h3').textContent === this.currentVersion.name);
+ item.classList.toggle('selected', item.dataset.versionId === versionId);
});
- // Update Next button state after selection
this.updateNextButtonState();
}
- // Update this method to use version-level existsLocally
updateNextButtonState() {
- const nextButton = document.querySelector('#versionStep .primary-btn');
+ const nextButton = document.getElementById('nextFromVersion');
if (!nextButton) return;
const existsLocally = this.currentVersion?.existsLocally;
@@ -240,7 +275,6 @@ export class DownloadManager {
return;
}
- // Double-check if the version exists locally
const existsLocally = this.currentVersion.existsLocally;
if (existsLocally) {
showToast('This version already exists in your library', 'info');
@@ -251,39 +285,30 @@ export class DownloadManager {
document.getElementById('locationStep').style.display = 'block';
try {
- // Fetch LoRA roots
- const rootsResponse = await fetch('/api/loras/roots');
- if (!rootsResponse.ok) {
- throw new Error('Failed to fetch LoRA roots');
- }
+ const config = this.apiClient.apiConfig.config;
- const rootsData = await rootsResponse.json();
- const loraRoot = document.getElementById('loraRoot');
- loraRoot.innerHTML = rootsData.roots.map(root =>
+ // Fetch model roots
+ const rootsData = await this.apiClient.fetchModelRoots();
+ const modelRoot = document.getElementById('modelRoot');
+ modelRoot.innerHTML = rootsData.roots.map(root =>
`
${root} `
).join('');
- // Set default lora root if available
- const defaultRoot = getStorageItem('settings', {}).default_loras_root;
+ // Set default root if available
+ const defaultRootKey = `default_${this.apiClient.modelType}_root`;
+ const defaultRoot = getStorageItem('settings', {})[defaultRootKey];
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
- loraRoot.value = defaultRoot;
+ modelRoot.value = defaultRoot;
}
- // Fetch folders dynamically
- const foldersResponse = await fetch('/api/loras/folders');
- if (!foldersResponse.ok) {
- throw new Error('Failed to fetch folders');
- }
-
- const foldersData = await foldersResponse.json();
+ // Fetch folders
+ const foldersData = await this.apiClient.fetchModelFolders();
const folderBrowser = document.getElementById('folderBrowser');
- // Update folder browser with dynamic content
folderBrowser.innerHTML = foldersData.folders.map(folder =>
`
${folder}
`
).join('');
- // Initialize folder browser after loading roots and folders
this.initializeFolderBrowser();
} catch (error) {
showToast(error.message, 'error');
@@ -300,12 +325,17 @@ export class DownloadManager {
document.getElementById('versionStep').style.display = 'block';
}
+ closeModal() {
+ modalManager.closeModal('downloadModal');
+ }
+
async startDownload() {
- const loraRoot = document.getElementById('loraRoot').value;
+ const modelRoot = document.getElementById('modelRoot').value;
const newFolder = document.getElementById('newFolder').value.trim();
+ const config = this.apiClient.apiConfig.config;
- if (!loraRoot) {
- showToast('Please select a LoRA root directory', 'error');
+ if (!modelRoot) {
+ showToast(`Please select a ${config.displayName} root directory`, 'error');
return;
}
@@ -320,38 +350,32 @@ export class DownloadManager {
}
try {
- // Show enhanced loading with progress details
const updateProgress = this.loadingManager.showDownloadProgress(1);
updateProgress(0, 0, this.currentVersion.name);
- // Generate a unique ID for this download
const downloadId = Date.now().toString();
- // Setup WebSocket for progress updates - use download-specific endpoint
+ // Setup WebSocket for progress updates
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
- // Handle download ID confirmation
if (data.type === 'download_id') {
console.log(`Connected to download progress with ID: ${data.download_id}`);
return;
}
- // Only process progress updates for our download
if (data.status === 'progress' && data.download_id === downloadId) {
- // Update progress display with current progress
updateProgress(data.progress, 0, this.currentVersion.name);
- // Add more detailed status messages based on progress
if (data.progress < 3) {
this.loadingManager.setStatus(`Preparing download...`);
} else if (data.progress === 3) {
this.loadingManager.setStatus(`Downloaded preview image`);
} else if (data.progress > 3 && data.progress < 100) {
- this.loadingManager.setStatus(`Downloading LoRA file`);
+ this.loadingManager.setStatus(`Downloading ${config.singularName} file`);
} else {
this.loadingManager.setStatus(`Finalizing download...`);
}
@@ -360,35 +384,47 @@ export class DownloadManager {
ws.onerror = (error) => {
console.error('WebSocket error:', error);
- // Continue with download even if WebSocket fails
};
- // Start download with our download ID
- const response = await fetch('/api/download-model', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- model_id: this.modelId,
- model_version_id: this.currentVersion.id,
- model_root: loraRoot,
- relative_path: targetFolder,
- download_id: downloadId
- })
- });
-
- if (!response.ok) {
- throw new Error(await response.text());
- }
+ // Start download
+ await this.apiClient.downloadModel(
+ this.modelId,
+ this.currentVersion.id,
+ modelRoot,
+ targetFolder,
+ downloadId
+ );
showToast('Download completed successfully', 'success');
modalManager.closeModal('downloadModal');
- // Close WebSocket after download completes
ws.close();
- // Update state and trigger reload with folder update
- state.activeFolder = targetFolder;
- await resetAndReload(true); // Pass true to update folders
+ // Update state and trigger reload
+ const pageState = this.apiClient.getPageState();
+ pageState.activeFolder = targetFolder;
+
+ // Save the active folder preference
+ setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder);
+
+ // Update UI folder selection
+ document.querySelectorAll('.folder-tags .tag').forEach(tag => {
+ const isActive = tag.dataset.folder === targetFolder;
+ tag.classList.toggle('active', isActive);
+ if (isActive && !tag.parentNode.classList.contains('collapsed')) {
+ tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ });
+
+ // Trigger reload with folder update - use dynamic import based on model type
+ const modelType = this.apiClient.modelType;
+ if (modelType === 'loras') {
+ const { resetAndReload } = await import('../api/loraApi.js');
+ await resetAndReload(true);
+ } else if (modelType === 'checkpoints') {
+ const { resetAndReload } = await import('../api/checkpointApi.js');
+ await resetAndReload(true);
+ }
} catch (error) {
showToast(error.message, 'error');
@@ -397,15 +433,12 @@ export class DownloadManager {
}
}
- // Add new method to handle folder selection
initializeFolderBrowser() {
const folderBrowser = document.getElementById('folderBrowser');
if (!folderBrowser) return;
- // Cleanup existing handler if any
this.cleanupFolderBrowser();
- // Create new handler
this.folderClickHandler = (event) => {
const folderItem = event.target.closest('.folder-item');
if (!folderItem) return;
@@ -420,21 +453,17 @@ export class DownloadManager {
this.selectedFolder = folderItem.dataset.folder;
}
- // Update path display after folder selection
this.updateTargetPath();
};
- // Add the new handler
folderBrowser.addEventListener('click', this.folderClickHandler);
- // Add event listeners for path updates
- const loraRoot = document.getElementById('loraRoot');
+ const modelRoot = document.getElementById('modelRoot');
const newFolder = document.getElementById('newFolder');
- loraRoot.addEventListener('change', this.updateTargetPath);
+ modelRoot.addEventListener('change', this.updateTargetPath);
newFolder.addEventListener('input', this.updateTargetPath);
- // Update initial path
this.updateTargetPath();
}
@@ -447,23 +476,22 @@ export class DownloadManager {
}
}
- // Remove path update listeners
- const loraRoot = document.getElementById('loraRoot');
+ const modelRoot = document.getElementById('modelRoot');
const newFolder = document.getElementById('newFolder');
- loraRoot.removeEventListener('change', this.updateTargetPath);
- newFolder.removeEventListener('input', this.updateTargetPath);
+ if (modelRoot) modelRoot.removeEventListener('change', this.updateTargetPath);
+ if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
}
- // Add new method to update target path
updateTargetPath() {
const pathDisplay = document.getElementById('targetPathDisplay');
- const loraRoot = document.getElementById('loraRoot').value;
+ const modelRoot = document.getElementById('modelRoot').value;
const newFolder = document.getElementById('newFolder').value.trim();
+ const config = this.apiClient.apiConfig.config;
- let fullPath = loraRoot || 'Select a LoRA root directory';
+ let fullPath = modelRoot || `Select a ${config.displayName} root directory`;
- if (loraRoot) {
+ if (modelRoot) {
if (this.selectedFolder) {
fullPath += '/' + this.selectedFolder;
}
@@ -475,3 +503,6 @@ export class DownloadManager {
pathDisplay.innerHTML = `
${fullPath} `;
}
}
+
+// Create global instance
+export const downloadManager = new DownloadManager();
diff --git a/templates/checkpoints.html b/templates/checkpoints.html
index 47f8295a..f65803ca 100644
--- a/templates/checkpoints.html
+++ b/templates/checkpoints.html
@@ -12,7 +12,6 @@
{% block init_check_url %}/api/checkpoints?page=1&page_size=1{% endblock %}
{% block additional_components %}
-{% include 'components/checkpoint_modals.html' %}