diff --git a/launcher/src/main/resources/webapp/css/styles.css b/launcher/src/main/resources/webapp/css/styles.css index 5adcf42..a0265d1 100644 --- a/launcher/src/main/resources/webapp/css/styles.css +++ b/launcher/src/main/resources/webapp/css/styles.css @@ -686,6 +686,30 @@ body { font-size: 13px; } +.progress-label { + margin-bottom: 8px; + font-weight: 500; +} + +.progress-file { + font-size: 12px; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.progress-fill.animated { + background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary), var(--accent-primary)); + background-size: 200% 100%; + animation: progressShimmer 1.5s ease-in-out infinite; +} + +@keyframes progressShimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + /* ==================== LOADING ==================== */ .loading-overlay { position: fixed; diff --git a/launcher/src/main/resources/webapp/js/app.js b/launcher/src/main/resources/webapp/js/app.js index a3142dd..15b730d 100644 --- a/launcher/src/main/resources/webapp/js/app.js +++ b/launcher/src/main/resources/webapp/js/app.js @@ -325,7 +325,43 @@ class App { } this.addLog('Обновление сборки...', 'info'); - this.showProgress('Обновление сборки...'); + this.enablePlayButton(false); + + const progressContainer = this.showAnimatedProgress('Обновление сборки...'); + + let eventSource = null; + let progressData = { totalFiles: 0, downloadedFiles: 0 }; + + try { + eventSource = new EventSource(`/api/instances/${this.currentInstance.name}/install/stream`); + eventSource.onmessage = (e) => { + try { + const data = JSON.parse(e.data); + if (data.phase === 'starting') { + progressData.totalFiles = data.totalFiles || 0; + this.updateAnimatedProgress(progressContainer, `Загрузка: 0/${progressData.totalFiles} файлов`, 5); + } else if (data.phase === 'downloading') { + progressData.downloadedFiles = data.downloadedFiles || 0; + const total = data.totalFiles || progressData.totalFiles || 1; + const percent = Math.round((progressData.downloadedFiles / total) * 100); + const fileName = data.currentFile ? data.currentFile.split('/').pop() : ''; + const filePercent = data.filePercent || 0; + this.updateAnimatedProgress(progressContainer, + `Файл ${progressData.downloadedFiles}/${total} (${percent}%)`, + percent, + fileName, + filePercent + ); + } else if (data.phase === 'complete') { + this.updateAnimatedProgress(progressContainer, 'Готово!', 100); + } else if (data.phase === 'error') { + this.addLog('Ошибка: ' + (data.message || 'неизвестная ошибка'), 'error'); + } + } catch (err) {} + }; + } catch (e) { + console.log('SSE not available, using fallback progress'); + } const result = await this.request('/instances/zernmc/install', { method: 'POST', @@ -335,6 +371,10 @@ class App { }) }); + if (eventSource) { + eventSource.close(); + } + this.hideProgress(); if (result.success) { @@ -362,6 +402,31 @@ class App { } } + showAnimatedProgress(text) { + const progress = document.getElementById('download-progress'); + const progressText = document.getElementById('progress-text'); + const progressFill = document.getElementById('progress-fill'); + + progress.classList.remove('hidden'); + progressText.innerHTML = `
${text}
+
`; + progressFill.style.width = '5%'; + progressFill.classList.add('animated'); + + return { container: progress, text: progressText, fill: progressFill }; + } + + updateAnimatedProgress(progressContainer, text, percent, fileName = '', filePercent = 0) { + const { text: progressText, fill: progressFill } = progressContainer; + if (fileName) { + progressText.innerHTML = `
${text}
+
${fileName} (${filePercent}%)
`; + } else { + progressText.innerHTML = `
${text}
`; + } + progressFill.style.width = percent + '%'; + } + // ==================== DOWNLOAD MODAL ==================== async showDownloadModal() { document.getElementById('download-modal').classList.remove('hidden');