feat: add file upload progress toast with per-file tracking (#93)
Replaced the hidden dropzone-only progress bar with a floating upload toast that appears at the bottom-right corner whenever files are being uploaded (button or drag-and-drop). Features: - Per-file progress bar with real byte-level tracking via XHR - Spinning icon while uploading, green check on success, red on error - Overall progress bar and file counter in the footer - Auto-hides 4 seconds after all uploads complete - Dismiss button to minimise the toast - Works for both file and folder uploads - i18n keys added to all 8 locale files - Service worker cache bumped to v3
This commit is contained in:
@@ -3473,3 +3473,132 @@ html[dir='rtl'] .fa-sign-out-alt {
|
|||||||
-webkit-transform: rotate(180deg);
|
-webkit-transform: rotate(180deg);
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Upload Progress Toast
|
||||||
|
============================================================================ */
|
||||||
|
.upload-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
width: 360px;
|
||||||
|
max-height: 400px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||||||
|
z-index: 10000;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: inherit;
|
||||||
|
animation: uploadToastSlideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.upload-toast.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@keyframes uploadToastSlideIn {
|
||||||
|
from { transform: translateY(20px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
.upload-toast-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #ff5e3a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.upload-toast-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.upload-toast-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.upload-toast-close:hover { opacity: 1; }
|
||||||
|
|
||||||
|
.upload-toast-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
.upload-toast-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 16px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.upload-toast-file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #999;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.upload-toast-file-icon.done { color: #34c759; }
|
||||||
|
.upload-toast-file-icon.error { color: #ff3b30; }
|
||||||
|
.upload-toast-file-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.upload-toast-file-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.upload-toast-file-bar {
|
||||||
|
height: 3px;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.upload-toast-file-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #ff5e3a;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.upload-toast-file-fill.done { background: #34c759; }
|
||||||
|
.upload-toast-file-fill.error { background: #ff3b30; }
|
||||||
|
.upload-toast-file-pct {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 38px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-toast-footer {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.upload-toast-overall-bar {
|
||||||
|
height: 4px;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.upload-toast-overall-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #ff5e3a;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.upload-toast-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
@@ -284,5 +284,22 @@
|
|||||||
<button class="about-close-btn" id="about-close-btn" data-i18n="actions.close">Close</button>
|
<button class="about-close-btn" id="about-close-btn" data-i18n="actions.close">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Progress Toast -->
|
||||||
|
<div class="upload-toast" id="upload-toast">
|
||||||
|
<div class="upload-toast-header">
|
||||||
|
<span class="upload-toast-title" id="upload-toast-title">Uploading...</span>
|
||||||
|
<button class="upload-toast-close" id="upload-toast-close" title="Minimize">−</button>
|
||||||
|
</div>
|
||||||
|
<div class="upload-toast-body" id="upload-toast-body">
|
||||||
|
<!-- File entries will be added dynamically -->
|
||||||
|
</div>
|
||||||
|
<div class="upload-toast-footer">
|
||||||
|
<div class="upload-toast-overall-bar">
|
||||||
|
<div class="upload-toast-overall-fill" id="upload-toast-overall-fill"></div>
|
||||||
|
</div>
|
||||||
|
<span class="upload-toast-stats" id="upload-toast-stats"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+199
-93
@@ -18,104 +18,217 @@ function getAuthHeaders() {
|
|||||||
|
|
||||||
// File Operations Module
|
// File Operations Module
|
||||||
const fileOps = {
|
const fileOps = {
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Upload progress toast helpers
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/** Show the upload progress toast and reset its contents */
|
||||||
|
_initUploadToast(totalFiles) {
|
||||||
|
const toast = document.getElementById('upload-toast');
|
||||||
|
const body = document.getElementById('upload-toast-body');
|
||||||
|
const title = document.getElementById('upload-toast-title');
|
||||||
|
const stats = document.getElementById('upload-toast-stats');
|
||||||
|
const fill = document.getElementById('upload-toast-overall-fill');
|
||||||
|
const closeBtn = document.getElementById('upload-toast-close');
|
||||||
|
|
||||||
|
body.innerHTML = '';
|
||||||
|
fill.style.width = '0%';
|
||||||
|
const uploadingText = (window.i18n && window.i18n.t) ? window.i18n.t('upload.uploading') : 'Uploading...';
|
||||||
|
title.textContent = uploadingText;
|
||||||
|
stats.textContent = `0 / ${totalFiles}`;
|
||||||
|
toast.classList.add('visible');
|
||||||
|
|
||||||
|
// Allow user to minimise (hide) the toast; it will re-appear on next upload
|
||||||
|
closeBtn.onclick = () => toast.classList.remove('visible');
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Add a file row to the toast and return its element references */
|
||||||
|
_addToastFileRow(fileName) {
|
||||||
|
const body = document.getElementById('upload-toast-body');
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'upload-toast-file';
|
||||||
|
row.innerHTML = `
|
||||||
|
<span class="upload-toast-file-icon"><i class="fas fa-spinner fa-spin"></i></span>
|
||||||
|
<div class="upload-toast-file-info">
|
||||||
|
<div class="upload-toast-file-name" title="${fileName}">${fileName}</div>
|
||||||
|
<div class="upload-toast-file-bar"><div class="upload-toast-file-fill"></div></div>
|
||||||
|
</div>
|
||||||
|
<span class="upload-toast-file-pct">0%</span>
|
||||||
|
`;
|
||||||
|
body.appendChild(row);
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
body.scrollTop = body.scrollHeight;
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
icon: row.querySelector('.upload-toast-file-icon'),
|
||||||
|
fill: row.querySelector('.upload-toast-file-fill'),
|
||||||
|
pct: row.querySelector('.upload-toast-file-pct'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Update the overall progress in the toast footer */
|
||||||
|
_updateOverallProgress(completedCount, totalFiles) {
|
||||||
|
const fill = document.getElementById('upload-toast-overall-fill');
|
||||||
|
const stats = document.getElementById('upload-toast-stats');
|
||||||
|
const pct = totalFiles > 0 ? Math.round((completedCount / totalFiles) * 100) : 0;
|
||||||
|
fill.style.width = pct + '%';
|
||||||
|
stats.textContent = `${completedCount} / ${totalFiles}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Mark upload toast as fully complete and auto-hide after a delay */
|
||||||
|
_finishUploadToast(successCount, totalFiles) {
|
||||||
|
const title = document.getElementById('upload-toast-title');
|
||||||
|
const fill = document.getElementById('upload-toast-overall-fill');
|
||||||
|
fill.style.width = '100%';
|
||||||
|
|
||||||
|
const completeText = (window.i18n && window.i18n.t)
|
||||||
|
? window.i18n.t('upload.complete', { count: successCount, total: totalFiles })
|
||||||
|
: `${successCount} / ${totalFiles} uploaded`;
|
||||||
|
title.textContent = completeText;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const toast = document.getElementById('upload-toast');
|
||||||
|
toast.classList.remove('visible');
|
||||||
|
}, 4000);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload files to the server
|
* Upload a single file via XMLHttpRequest with progress events.
|
||||||
|
* Returns a promise that resolves with { ok, data? }.
|
||||||
|
*/
|
||||||
|
_uploadFileXHR(formData, fileRowElements) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable && fileRowElements) {
|
||||||
|
const pct = Math.round((e.loaded / e.total) * 100);
|
||||||
|
fileRowElements.fill.style.width = pct + '%';
|
||||||
|
fileRowElements.pct.textContent = pct + '%';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
if (fileRowElements) {
|
||||||
|
fileRowElements.fill.style.width = '100%';
|
||||||
|
fileRowElements.fill.classList.add('done');
|
||||||
|
fileRowElements.pct.textContent = '100%';
|
||||||
|
fileRowElements.icon.innerHTML = '<i class="fas fa-check-circle"></i>';
|
||||||
|
fileRowElements.icon.classList.add('done');
|
||||||
|
}
|
||||||
|
let data = null;
|
||||||
|
try { data = JSON.parse(xhr.responseText); } catch (_) {}
|
||||||
|
resolve({ ok: true, data });
|
||||||
|
} else {
|
||||||
|
if (fileRowElements) {
|
||||||
|
fileRowElements.fill.classList.add('error');
|
||||||
|
fileRowElements.pct.textContent = 'ERR';
|
||||||
|
fileRowElements.icon.innerHTML = '<i class="fas fa-exclamation-circle"></i>';
|
||||||
|
fileRowElements.icon.classList.add('error');
|
||||||
|
}
|
||||||
|
resolve({ ok: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
if (fileRowElements) {
|
||||||
|
fileRowElements.fill.classList.add('error');
|
||||||
|
fileRowElements.pct.textContent = 'ERR';
|
||||||
|
fileRowElements.icon.innerHTML = '<i class="fas fa-exclamation-circle"></i>';
|
||||||
|
fileRowElements.icon.classList.add('error');
|
||||||
|
}
|
||||||
|
resolve({ ok: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open('POST', '/api/files/upload');
|
||||||
|
|
||||||
|
// Set auth header
|
||||||
|
const token = localStorage.getItem('oxicloud_token');
|
||||||
|
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||||
|
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Upload files (via button or drag-and-drop)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload files to the server with real-time progress indication
|
||||||
* @param {FileList} files - Files to upload
|
* @param {FileList} files - Files to upload
|
||||||
*/
|
*/
|
||||||
async uploadFiles(files) {
|
async uploadFiles(files) {
|
||||||
|
const totalFiles = files.length;
|
||||||
|
if (totalFiles === 0) return;
|
||||||
|
|
||||||
|
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
||||||
const progressBar = document.querySelector('.progress-fill');
|
const progressBar = document.querySelector('.progress-fill');
|
||||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||||
uploadProgressDiv.style.display = 'block';
|
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
||||||
progressBar.style.width = '0%';
|
if (progressBar) { progressBar.style.width = '0%'; }
|
||||||
|
|
||||||
|
// Show upload toast
|
||||||
|
this._initUploadToast(totalFiles);
|
||||||
|
|
||||||
let uploadedCount = 0;
|
let uploadedCount = 0;
|
||||||
const totalFiles = files.length;
|
let successCount = 0;
|
||||||
|
|
||||||
for (let i = 0; i < totalFiles; i++) {
|
for (let i = 0; i < totalFiles; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// IMPORTANT: folder_id MUST be added BEFORE file for multipart processing
|
|
||||||
// The backend reads fields in order, and needs folder_id before processing the file
|
|
||||||
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||||
|
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
||||||
if (targetFolderId) {
|
|
||||||
formData.append('folder_id', targetFolderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the file AFTER folder_id
|
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
try {
|
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`, {
|
||||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`);
|
file: file.name, size: file.size
|
||||||
|
|
||||||
// We use the correct URL for file upload
|
|
||||||
console.log('Form to submit:', {
|
|
||||||
file: file.name,
|
|
||||||
size: file.size,
|
|
||||||
folder_id: targetFolderId || 'root'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch('/api/files/upload', {
|
// Add row to toast
|
||||||
method: 'POST',
|
const rowEls = this._addToastFileRow(file.name);
|
||||||
body: formData,
|
|
||||||
// Add cache: 'no-store' to avoid cache issues during upload
|
|
||||||
cache: 'no-store',
|
|
||||||
headers: {
|
|
||||||
...getAuthHeaders(),
|
|
||||||
// Add this header to force fresh reloads
|
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Server response:', {
|
const result = await this._uploadFileXHR(formData, rowEls);
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
const percentComplete = (uploadedCount / totalFiles) * 100;
|
|
||||||
progressBar.style.width = percentComplete + '%';
|
|
||||||
|
|
||||||
if (response.ok) {
|
// Legacy dropzone bar
|
||||||
const responseData = await response.json();
|
if (progressBar) {
|
||||||
console.log(`Successfully uploaded ${file.name}`, responseData);
|
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
||||||
|
}
|
||||||
|
// Toast overall bar
|
||||||
|
this._updateOverallProgress(uploadedCount, totalFiles);
|
||||||
|
|
||||||
// Show success notification immediately
|
if (result.ok) {
|
||||||
window.ui.showNotification('File uploaded', `${file.name} completed`);
|
successCount++;
|
||||||
|
console.log(`Successfully uploaded ${file.name}`, result.data);
|
||||||
|
} else {
|
||||||
|
console.error(`Upload error for ${file.name}`);
|
||||||
|
window.ui.showNotification('Error', `Error uploading file: ${file.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (i === totalFiles - 1) {
|
// All done
|
||||||
// Last file uploaded - wait and reload once
|
this._finishUploadToast(successCount, totalFiles);
|
||||||
console.log('Last file uploaded, waiting before reloading...');
|
|
||||||
|
|
||||||
// Wait for backend to persist
|
// Wait for backend to persist, then reload
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
// Single reload with force refresh
|
|
||||||
try {
|
try {
|
||||||
await window.loadFiles({ forceRefresh: true });
|
await window.loadFiles({ forceRefresh: true });
|
||||||
} catch (reloadError) {
|
} catch (reloadError) {
|
||||||
console.error("Error reloading files:", reloadError);
|
console.error('Error reloading files:', reloadError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide upload UI
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const dropzone = document.getElementById('dropzone');
|
const dropzone = document.getElementById('dropzone');
|
||||||
if (dropzone) dropzone.style.display = 'none';
|
if (dropzone) dropzone.style.display = 'none';
|
||||||
uploadProgressDiv.style.display = 'none';
|
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorData = await response.text();
|
|
||||||
console.error('Upload error:', errorData);
|
|
||||||
window.ui.showNotification('Error', `Error uploading file: ${file.name}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Network error during upload:', error);
|
|
||||||
window.ui.showNotification('Error', `Network error uploading file: ${file.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,33 +241,29 @@ const fileOps = {
|
|||||||
|
|
||||||
const progressBar = document.querySelector('.progress-fill');
|
const progressBar = document.querySelector('.progress-fill');
|
||||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||||
uploadProgressDiv.style.display = 'block';
|
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
||||||
progressBar.style.width = '0%';
|
if (progressBar) { progressBar.style.width = '0%'; }
|
||||||
|
|
||||||
const currentFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
const currentFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||||
|
|
||||||
// Build folder structure from relative paths
|
// Build folder structure from relative paths
|
||||||
// webkitRelativePath looks like: "folderName/subfolder/file.txt"
|
const folderMap = new Map();
|
||||||
const folderMap = new Map(); // path -> folder_id
|
folderMap.set('', currentFolderId);
|
||||||
folderMap.set('', currentFolderId); // root = current folder
|
|
||||||
|
|
||||||
// Collect all unique folder paths
|
|
||||||
const folderPaths = new Set();
|
const folderPaths = new Set();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const parts = file.webkitRelativePath.split('/');
|
const parts = file.webkitRelativePath.split('/');
|
||||||
// Remove filename, keep folder parts
|
|
||||||
for (let i = 1; i < parts.length; i++) {
|
for (let i = 1; i < parts.length; i++) {
|
||||||
const path = parts.slice(0, i).join('/');
|
const path = parts.slice(0, i).join('/');
|
||||||
folderPaths.add(path);
|
folderPaths.add(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort paths by depth so parents are created first
|
|
||||||
const sortedPaths = [...folderPaths].sort((a, b) =>
|
const sortedPaths = [...folderPaths].sort((a, b) =>
|
||||||
a.split('/').length - b.split('/').length
|
a.split('/').length - b.split('/').length
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create folders
|
// Create folders first (no progress toast for folder creation)
|
||||||
for (const folderPath of sortedPaths) {
|
for (const folderPath of sortedPaths) {
|
||||||
const parts = folderPath.split('/');
|
const parts = folderPath.split('/');
|
||||||
const folderName = parts[parts.length - 1];
|
const folderName = parts[parts.length - 1];
|
||||||
@@ -188,9 +297,12 @@ const fileOps = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload files into their respective folders
|
// Upload files with progress toast
|
||||||
let uploadedCount = 0;
|
|
||||||
const totalFiles = files.length;
|
const totalFiles = files.length;
|
||||||
|
this._initUploadToast(totalFiles);
|
||||||
|
|
||||||
|
let uploadedCount = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
for (let i = 0; i < totalFiles; i++) {
|
for (let i = 0; i < totalFiles; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
@@ -202,33 +314,27 @@ const fileOps = {
|
|||||||
formData.append('folder_id', targetFolderId);
|
formData.append('folder_id', targetFolderId);
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
try {
|
const displayName = file.webkitRelativePath || file.name;
|
||||||
const response = await fetch('/api/files/upload', {
|
const rowEls = this._addToastFileRow(displayName);
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
const result = await this._uploadFileXHR(formData, rowEls);
|
||||||
cache: 'no-store',
|
|
||||||
headers: {
|
|
||||||
...getAuthHeaders(),
|
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadedCount++;
|
uploadedCount++;
|
||||||
const percentComplete = (uploadedCount / totalFiles) * 100;
|
if (progressBar) {
|
||||||
progressBar.style.width = percentComplete + '%';
|
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
||||||
|
}
|
||||||
|
this._updateOverallProgress(uploadedCount, totalFiles);
|
||||||
|
|
||||||
if (response.ok) {
|
if (result.ok) {
|
||||||
|
successCount++;
|
||||||
console.log(`Uploaded: ${file.webkitRelativePath}`);
|
console.log(`Uploaded: ${file.webkitRelativePath}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error uploading ${file.webkitRelativePath}:`, await response.text());
|
console.error(`Error uploading ${file.webkitRelativePath}`);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Network error uploading ${file.webkitRelativePath}:`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish up
|
// Finish
|
||||||
window.ui.showNotification('Folder uploaded', `${uploadedCount} files uploaded successfully`);
|
this._finishUploadToast(successCount, totalFiles);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
@@ -241,7 +347,7 @@ const fileOps = {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const dropzone = document.getElementById('dropzone');
|
const dropzone = document.getElementById('dropzone');
|
||||||
if (dropzone) dropzone.style.display = 'none';
|
if (dropzone) dropzone.style.display = 'none';
|
||||||
uploadProgressDiv.style.display = 'none';
|
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "Hochladen",
|
"upload": "Hochladen",
|
||||||
"upload_files": "Dateien hochladen",
|
"upload_files": "Dateien hochladen",
|
||||||
"upload_folder": "Ordner hochladen",
|
"upload_folder": "Ordner hochladen",
|
||||||
|
"upload.uploading": "Wird hochgeladen...",
|
||||||
|
"upload.complete": "{count} / {total} hochgeladen",
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"move": "Verschieben nach...",
|
"move": "Verschieben nach...",
|
||||||
"move_to": "Verschieben nach",
|
"move_to": "Verschieben nach",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload_files": "Upload files",
|
"upload_files": "Upload files",
|
||||||
"upload_folder": "Upload folder",
|
"upload_folder": "Upload folder",
|
||||||
|
"upload.uploading": "Uploading...",
|
||||||
|
"upload.complete": "{count} / {total} uploaded",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"move": "Move to...",
|
"move": "Move to...",
|
||||||
"move_to": "Move to",
|
"move_to": "Move to",
|
||||||
|
|||||||
@@ -135,6 +135,8 @@
|
|||||||
"upload": "Subir",
|
"upload": "Subir",
|
||||||
"upload_files": "Subir archivos",
|
"upload_files": "Subir archivos",
|
||||||
"upload_folder": "Subir carpeta",
|
"upload_folder": "Subir carpeta",
|
||||||
|
"upload.uploading": "Subiendo...",
|
||||||
|
"upload.complete": "{count} / {total} subidos",
|
||||||
"rename": "Renombrar",
|
"rename": "Renombrar",
|
||||||
"move": "Mover a...",
|
"move": "Mover a...",
|
||||||
"move_to": "Mover a",
|
"move_to": "Mover a",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "بارگذاری",
|
"upload": "بارگذاری",
|
||||||
"upload_files": "بارگذاری پروندهها",
|
"upload_files": "بارگذاری پروندهها",
|
||||||
"upload_folder": "بارگذاری پوشه",
|
"upload_folder": "بارگذاری پوشه",
|
||||||
|
"upload.uploading": "...در حال بارگذاری",
|
||||||
|
"upload.complete": "{count} / {total} بارگذاری شد",
|
||||||
"rename": "تغییر نام",
|
"rename": "تغییر نام",
|
||||||
"move": "انتقال به...",
|
"move": "انتقال به...",
|
||||||
"move_to": "انتقال به",
|
"move_to": "انتقال به",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "Téléverser",
|
"upload": "Téléverser",
|
||||||
"upload_files": "Téléverser des fichiers",
|
"upload_files": "Téléverser des fichiers",
|
||||||
"upload_folder": "Téléverser un dossier",
|
"upload_folder": "Téléverser un dossier",
|
||||||
|
"upload.uploading": "Envoi en cours...",
|
||||||
|
"upload.complete": "{count} / {total} envoyés",
|
||||||
"rename": "Renommer",
|
"rename": "Renommer",
|
||||||
"move": "Déplacer vers...",
|
"move": "Déplacer vers...",
|
||||||
"move_to": "Déplacer vers",
|
"move_to": "Déplacer vers",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "Carica",
|
"upload": "Carica",
|
||||||
"upload_files": "Carica file",
|
"upload_files": "Carica file",
|
||||||
"upload_folder": "Carica cartella",
|
"upload_folder": "Carica cartella",
|
||||||
|
"upload.uploading": "Caricamento...",
|
||||||
|
"upload.complete": "{count} / {total} caricati",
|
||||||
"rename": "Rinomina",
|
"rename": "Rinomina",
|
||||||
"move": "Sposta in...",
|
"move": "Sposta in...",
|
||||||
"move_to": "Sposta in",
|
"move_to": "Sposta in",
|
||||||
@@ -311,7 +313,7 @@
|
|||||||
"fa": "Persiano",
|
"fa": "Persiano",
|
||||||
"fr": "Francese",
|
"fr": "Francese",
|
||||||
"de": "Tedesco",
|
"de": "Tedesco",
|
||||||
"pt": "Portoghese",
|
"pt": "Portoghese"
|
||||||
"it": "Italiano"
|
"it": "Italiano"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "Enviar",
|
"upload": "Enviar",
|
||||||
"upload_files": "Enviar arquivos",
|
"upload_files": "Enviar arquivos",
|
||||||
"upload_folder": "Enviar pasta",
|
"upload_folder": "Enviar pasta",
|
||||||
|
"upload.uploading": "Enviando...",
|
||||||
|
"upload.complete": "{count} / {total} enviados",
|
||||||
"rename": "Renomear",
|
"rename": "Renomear",
|
||||||
"move": "Mover para...",
|
"move": "Mover para...",
|
||||||
"move_to": "Mover para",
|
"move_to": "Mover para",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
"upload_files": "上传文件",
|
"upload_files": "上传文件",
|
||||||
"upload_folder": "上传文件夹",
|
"upload_folder": "上传文件夹",
|
||||||
|
"upload.uploading": "上传中...",
|
||||||
|
"upload.complete": "{count} / {total} 已上传",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"move": "移动到...",
|
"move": "移动到...",
|
||||||
"move_to": "移动到",
|
"move_to": "移动到",
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// OxiCloud Service Worker
|
// OxiCloud Service Worker
|
||||||
const CACHE_NAME = 'oxicloud-cache-v2';
|
const CACHE_NAME = 'oxicloud-cache-v3';
|
||||||
const ASSETS_TO_CACHE = [
|
const ASSETS_TO_CACHE = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user