import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { LoadingManager } from './LoadingManager.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; import { FolderTreeManager } from '../components/FolderTreeManager.js'; import { translate } from '../utils/i18nHelpers.js'; export class DownloadManager { constructor() { this.currentVersion = null; this.versions = []; this.modelInfo = null; 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); this.handleStartDownload = this.startDownload.bind(this); this.handleBackToUrl = this.backToUrl.bind(this); this.handleBackToVersions = this.backToVersions.bind(this); this.handleCloseModal = this.closeModal.bind(this); this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this); } 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) { 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, () => { this.cleanupFolderBrowser(); }); this.resetSteps(); // Auto-focus on the URL input setTimeout(() => { const urlInput = document.getElementById('modelUrl'); if (urlInput) { urlInput.focus(); } }, 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); // 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 => { if (label.textContent.includes('Location Preview')) { label.textContent = translate('modals.download.locationPreview') + ':'; } }); // Update initial path text const pathText = document.querySelector('#targetPathDisplay .path-text'); if (pathText) { pathText.textContent = translate('modals.download.selectTypeRoot', { type: config.displayName }); } } resetSteps() { document.querySelectorAll('.download-step').forEach(step => step.style.display = 'none'); 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(); } 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')); } this.versions = await this.apiClient.fetchCivitaiVersions(this.modelId, this.source); if (!this.versions.length) { throw new Error(translate('modals.download.errors.noVersions')); } // 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 versionMatch = url.match(/modelVersionId=(\d+)/i); this.modelVersionId = versionMatch ? versionMatch[1] : null; const civarchiveMatch = url.match(/https?:\/\/(?:www\.)?(?:civitaiarchive|civarchive)\.com\/models\/(\d+)/i); if (civarchiveMatch) { this.source = 'civarchive'; return civarchiveMatch[1]; } const civitaiMatch = url.match(/https?:\/\/(?:www\.)?civitai\.com\/models\/(\d+)/i); if (civitaiMatch) { this.source = null; return civitaiMatch[1]; } this.source = null; return null; } 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) : (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 = `