diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index f58200f3..08c0a0be 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -341,6 +341,90 @@ width: 60px; text-align: center; } + /* Failed uploads banner */ + .failed-uploads-banner { + background-color: #fff3cd; + border: 1px solid #ffc107; + border-radius: 4px; + padding: 15px; + margin-bottom: 15px; + display: none; + } + .failed-uploads-banner.show { + display: block; + } + .failed-uploads-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + } + .failed-uploads-title { + font-weight: 600; + color: #856404; + margin: 0; + } + .dismiss-btn { + background: none; + border: none; + font-size: 1.2em; + cursor: pointer; + color: #856404; + padding: 0; + line-height: 1; + } + .dismiss-btn:hover { + color: #533f03; + } + .failed-file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #ffe69c; + } + .failed-file-item:last-child { + border-bottom: none; + } + .failed-file-info { + flex: 1; + } + .failed-file-name { + font-weight: 500; + color: #856404; + } + .failed-file-error { + font-size: 0.85em; + color: #856404; + opacity: 0.8; + } + .retry-btn { + background-color: #ffc107; + color: #533f03; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + font-weight: 600; + } + .retry-btn:hover { + background-color: #e0a800; + } + .retry-all-btn { + background-color: #ffc107; + color: #533f03; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 0.95em; + font-weight: 600; + margin-top: 10px; + } + .retry-all-btn:hover { + background-color: #e0a800; + } /* Delete modal */ .delete-warning { color: #e74c3c; @@ -505,6 +589,16 @@ + +
+
+

⚠️ Some files failed to upload

+ +
+
+ +
+

Contents

@@ -531,7 +625,7 @@

📤 Upload file

Select a file to upload to

- +
@@ -717,65 +811,183 @@ function validateFile() { const fileInput = document.getElementById('fileInput'); const uploadBtn = document.getElementById('uploadBtn'); - const file = fileInput.files[0]; - uploadBtn.disabled = !file; + const files = fileInput.files; + uploadBtn.disabled = !(files.length > 0); } - function uploadFile() { - const fileInput = document.getElementById('fileInput'); - const file = fileInput.files[0]; +let failedUploadsGlobal = []; - if (!file) { - alert('Please select a file first!'); +function uploadFile() { + const fileInput = document.getElementById('fileInput'); + const files = Array.from(fileInput.files); + + if (files.length === 0) { + alert('Please select at least one file!'); + return; + } + + const progressContainer = document.getElementById('progress-container'); + const progressFill = document.getElementById('progress-fill'); + const progressText = document.getElementById('progress-text'); + const uploadBtn = document.getElementById('uploadBtn'); + + progressContainer.style.display = 'block'; + uploadBtn.disabled = true; + + let currentIndex = 0; + const failedFiles = []; + + function uploadNextFile() { + if (currentIndex >= files.length) { + // All files processed - show summary + if (failedFiles.length === 0) { + progressFill.style.backgroundColor = '#4caf50'; + progressText.textContent = 'All uploads complete!'; + setTimeout(() => { + closeUploadModal(); + hydrate(); // Refresh file list instead of reloading + }, 1000); + } else { + progressFill.style.backgroundColor = '#e74c3c'; + const failedList = failedFiles.map(f => f.name).join(', '); + progressText.textContent = `${files.length - failedFiles.length}/${files.length} uploaded. Failed: ${failedList}`; + + // Store failed files globally and show banner + failedUploadsGlobal = failedFiles; + + setTimeout(() => { + closeUploadModal(); + showFailedUploadsBanner(); + hydrate(); // Refresh file list to show successfully uploaded files + }, 2000); + } return; } + const file = files[currentIndex]; const formData = new FormData(); formData.append('file', file); - const progressContainer = document.getElementById('progress-container'); - const progressFill = document.getElementById('progress-fill'); - const progressText = document.getElementById('progress-text'); - const uploadBtn = document.getElementById('uploadBtn'); - - progressContainer.style.display = 'block'; - uploadBtn.disabled = true; - const xhr = new XMLHttpRequest(); // Include path as query parameter since multipart form data doesn't make // form fields available until after file upload completes xhr.open('POST', '/upload?path=' + encodeURIComponent(currentPath), true); - xhr.upload.onprogress = function(e) { + progressFill.style.width = '0%'; + progressFill.style.backgroundColor = '#4caf50'; + progressText.textContent = `Uploading ${file.name} (${currentIndex + 1}/${files.length})`; + + xhr.upload.onprogress = function (e) { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); progressFill.style.width = percent + '%'; - progressText.textContent = 'Uploading: ' + percent + '%'; + progressText.textContent = + `Uploading ${file.name} (${currentIndex + 1}/${files.length}) — ${percent}%`; } }; - xhr.onload = function() { + xhr.onload = function () { if (xhr.status === 200) { - progressText.textContent = 'Upload complete!'; - setTimeout(function() { - window.location.reload(); - }, 1000); + currentIndex++; + uploadNextFile(); // upload next file } else { - progressText.textContent = 'Upload failed: ' + xhr.responseText; - progressFill.style.backgroundColor = '#e74c3c'; - uploadBtn.disabled = false; + // Track failure and continue with next file + failedFiles.push({ name: file.name, error: xhr.responseText, file: file }); + currentIndex++; + uploadNextFile(); } }; - xhr.onerror = function() { - progressText.textContent = 'Upload failed - network error'; - progressFill.style.backgroundColor = '#e74c3c'; - uploadBtn.disabled = false; + xhr.onerror = function () { + // Track network error and continue with next file + failedFiles.push({ name: file.name, error: 'network error', file: file }); + currentIndex++; + uploadNextFile(); }; xhr.send(formData); } + uploadNextFile(); +} + +function showFailedUploadsBanner() { + const banner = document.getElementById('failedUploadsBanner'); + const filesList = document.getElementById('failedFilesList'); + + filesList.innerHTML = ''; + + failedUploadsGlobal.forEach((failedFile, index) => { + const item = document.createElement('div'); + item.className = 'failed-file-item'; + item.innerHTML = ` +
+
📄 ${escapeHtml(failedFile.name)}
+
Error: ${escapeHtml(failedFile.error)}
+
+ + `; + filesList.appendChild(item); + }); + + // Ensure retry all button is visible + const retryAllBtn = banner.querySelector('.retry-all-btn'); + if (retryAllBtn) retryAllBtn.style.display = ''; + + banner.classList.add('show'); +} + +function dismissFailedUploads() { + const banner = document.getElementById('failedUploadsBanner'); + banner.classList.remove('show'); + failedUploadsGlobal = []; +} + +function retrySingleUpload(index) { + const failedFile = failedUploadsGlobal[index]; + if (!failedFile) return; + + // Create a DataTransfer to set the file input + const dt = new DataTransfer(); + dt.items.add(failedFile.file); + + const fileInput = document.getElementById('fileInput'); + fileInput.files = dt.files; + + // Remove this file from failed list + failedUploadsGlobal.splice(index, 1); + + // If no more failed files, hide banner + if (failedUploadsGlobal.length === 0) { + dismissFailedUploads(); + } + + // Open modal and trigger upload + openUploadModal(); + validateFile(); +} + +function retryAllFailedUploads() { + if (failedUploadsGlobal.length === 0) return; + + // Create a DataTransfer with all failed files + const dt = new DataTransfer(); + failedUploadsGlobal.forEach(failedFile => { + dt.items.add(failedFile.file); + }); + + const fileInput = document.getElementById('fileInput'); + fileInput.files = dt.files; + + // Clear failed files list + failedUploadsGlobal = []; + dismissFailedUploads(); + + // Open modal and trigger upload + openUploadModal(); + validateFile(); +} + function createFolder() { const folderName = document.getElementById('folderName').value.trim();