Spaces:
Paused
Paused
Update public/app.js
Browse files- public/app.js +124 -42
public/app.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
| 1 |
// ==========================================
|
| 2 |
-
// PIXELDRIVE v4.
|
| 3 |
// ==========================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const app = {
|
| 5 |
user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
|
| 6 |
files: [], folders: [], uploadController: null, _uploadCancelled: false,
|
|
@@ -8,7 +21,6 @@ const app = {
|
|
| 8 |
_dialogResolver: null, _dialogOpts: null,
|
| 9 |
|
| 10 |
async init() {
|
| 11 |
-
// ✅ Préférences locales (thème, tri) + i18n
|
| 12 |
document.body.classList.toggle('light', (localStorage.getItem('pd_theme') || 'dark') === 'light');
|
| 13 |
this.sortMode = localStorage.getItem('pd_sort') || 'name-asc';
|
| 14 |
const ss = document.getElementById('sort-select'); if (ss) ss.value = this.sortMode;
|
|
@@ -39,6 +51,11 @@ const app = {
|
|
| 39 |
formatSize(b) { if (b === null || b === undefined) return '—'; if (b < 1024) return b + ' B'; if (b < 1048576) return (b/1024).toFixed(0) + ' KB'; if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB'; return (b/1073741824).toFixed(2) + ' GB'; },
|
| 40 |
statusLabel(s) { return ({pending:t('st_pending'), uploading:t('st_uploading'), processing:t('st_processing'), ready:t('st_ready'), error:t('st_error')})[s] || s; },
|
| 41 |
formatDate(ts) { return ts ? new Date(ts).toLocaleString('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
toast(msg, type) {
|
| 43 |
const el = document.createElement('div');
|
| 44 |
el.className = 'toast ' + (type || 'info');
|
|
@@ -47,7 +64,6 @@ const app = {
|
|
| 47 |
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
|
| 48 |
},
|
| 49 |
|
| 50 |
-
// ✅ Lignes composites (texte + lien) traduites
|
| 51 |
_switchHTML(key, onclick) {
|
| 52 |
const s = t(key);
|
| 53 |
const i = s.indexOf('?');
|
|
@@ -61,7 +77,7 @@ const app = {
|
|
| 61 |
if (sw2) sw2.innerHTML = this._switchHTML('have_account', "app.toggleAuth('login')");
|
| 62 |
},
|
| 63 |
|
| 64 |
-
//
|
| 65 |
dialog(opts) {
|
| 66 |
return new Promise(resolve => {
|
| 67 |
this._dialogResolver = resolve;
|
|
@@ -141,7 +157,7 @@ const app = {
|
|
| 141 |
hideTos() { document.getElementById('tos-modal').style.display = 'none'; },
|
| 142 |
async acceptTos() { await fetch('/api/tos/accept', {method:'POST'}); this.user.tosAccepted = true; this.hideTos(); this.toast('CGU ✓', 'success'); },
|
| 143 |
|
| 144 |
-
//
|
| 145 |
loadAvatar() {
|
| 146 |
if (!this.user) return;
|
| 147 |
const url = '/api/users/' + this.user.id + '/avatar?' + Date.now();
|
|
@@ -173,7 +189,7 @@ const app = {
|
|
| 173 |
this.loadAvatar();
|
| 174 |
},
|
| 175 |
|
| 176 |
-
//
|
| 177 |
openSettings() {
|
| 178 |
document.getElementById('settings-modal').style.display = 'flex';
|
| 179 |
document.getElementById('set-lang').value = getLang();
|
|
@@ -299,7 +315,7 @@ const app = {
|
|
| 299 |
el.addEventListener('touchmove', () => clearTimeout(t2));
|
| 300 |
},
|
| 301 |
|
| 302 |
-
//
|
| 303 |
async loadSharedPage() {
|
| 304 |
const area = document.getElementById('file-area');
|
| 305 |
area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
|
|
@@ -345,7 +361,6 @@ const app = {
|
|
| 345 |
const list = document.createElement('div'); list.className = 'share-list';
|
| 346 |
myShares.forEach(sh => {
|
| 347 |
const name = sh.file_name || sh.folder_name || '—';
|
| 348 |
-
const kind = sh.file_id ? t('folder').replace(t('folder'), sh.file_id ? 'Fichier' : 'Dossier') : 'Dossier';
|
| 349 |
const item = document.createElement('div'); item.className = 'share-item';
|
| 350 |
item.innerHTML =
|
| 351 |
'<div class="share-item-header"><div class="share-item-info"><svg class="icon icon-sm"><use href="#' + (sh.file_id ? 'icon-file' : 'icon-folder') + '"></use></svg><div style="min-width:0;"><div class="share-item-name">' + this.esc(name) + '</div><div class="share-item-sub">' + (sh.file_id ? 'Fichier' : 'Dossier') + ' · ' + t('shared_with') + ' <strong style="color:var(--primary);">' + this.esc(sh.shared_with_name) + '</strong></div></div></div><button class="btn-icon myshare-revoke" data-id="' + sh.id + '"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
|
|
@@ -406,7 +421,9 @@ const app = {
|
|
| 406 |
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 407 |
},
|
| 408 |
|
| 409 |
-
//
|
|
|
|
|
|
|
| 410 |
async handleUpload(input) {
|
| 411 |
const files = Array.from(input.files);
|
| 412 |
input.value = '';
|
|
@@ -424,44 +441,109 @@ const app = {
|
|
| 424 |
},
|
| 425 |
|
| 426 |
uploadOne(file, idx, total) {
|
| 427 |
-
return new Promise(
|
| 428 |
const t0 = performance.now();
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
document.getElementById('upload-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
const fd = new FormData();
|
| 435 |
fd.append('file', file);
|
| 436 |
if (this.currentFolder) fd.append('folder_id', this.currentFolder.id);
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
| 443 |
};
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
const
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
});
|
| 466 |
},
|
| 467 |
|
|
|
|
| 1 |
// ==========================================
|
| 2 |
+
// PIXELDRIVE v4.2 - APP.JS (progression upload réelle + stats)
|
| 3 |
// ==========================================
|
| 4 |
+
|
| 5 |
+
// ✅ Nouvelles clés de traduction (injectées dans le dictionnaire existant)
|
| 6 |
+
if (window.I18N) {
|
| 7 |
+
Object.assign(window.I18N.fr, {
|
| 8 |
+
up_elapsed: 'Temps', up_speed: 'Vitesse', up_eta: 'Restant',
|
| 9 |
+
up_done_in: 'Terminé en', up_phase_upload: 'Envoi au serveur', up_phase_process: 'Traitement'
|
| 10 |
+
});
|
| 11 |
+
Object.assign(window.I18N.en, {
|
| 12 |
+
up_elapsed: 'Time', up_speed: 'Speed', up_eta: 'Left',
|
| 13 |
+
up_done_in: 'Done in', up_phase_upload: 'Uploading to server', up_phase_process: 'Processing'
|
| 14 |
+
});
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
const app = {
|
| 18 |
user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
|
| 19 |
files: [], folders: [], uploadController: null, _uploadCancelled: false,
|
|
|
|
| 21 |
_dialogResolver: null, _dialogOpts: null,
|
| 22 |
|
| 23 |
async init() {
|
|
|
|
| 24 |
document.body.classList.toggle('light', (localStorage.getItem('pd_theme') || 'dark') === 'light');
|
| 25 |
this.sortMode = localStorage.getItem('pd_sort') || 'name-asc';
|
| 26 |
const ss = document.getElementById('sort-select'); if (ss) ss.value = this.sortMode;
|
|
|
|
| 51 |
formatSize(b) { if (b === null || b === undefined) return '—'; if (b < 1024) return b + ' B'; if (b < 1048576) return (b/1024).toFixed(0) + ' KB'; if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB'; return (b/1073741824).toFixed(2) + ' GB'; },
|
| 52 |
statusLabel(s) { return ({pending:t('st_pending'), uploading:t('st_uploading'), processing:t('st_processing'), ready:t('st_ready'), error:t('st_error')})[s] || s; },
|
| 53 |
formatDate(ts) { return ts ? new Date(ts).toLocaleString('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; },
|
| 54 |
+
fmtDur(s) {
|
| 55 |
+
s = Math.max(0, Math.round(s));
|
| 56 |
+
const m = Math.floor(s / 60), r = s % 60;
|
| 57 |
+
return m > 0 ? m + 'min ' + String(r).padStart(2, '0') + 's' : r + 's';
|
| 58 |
+
},
|
| 59 |
toast(msg, type) {
|
| 60 |
const el = document.createElement('div');
|
| 61 |
el.className = 'toast ' + (type || 'info');
|
|
|
|
| 64 |
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
|
| 65 |
},
|
| 66 |
|
|
|
|
| 67 |
_switchHTML(key, onclick) {
|
| 68 |
const s = t(key);
|
| 69 |
const i = s.indexOf('?');
|
|
|
|
| 77 |
if (sw2) sw2.innerHTML = this._switchHTML('have_account', "app.toggleAuth('login')");
|
| 78 |
},
|
| 79 |
|
| 80 |
+
// ---------- DIALOGUES ----------
|
| 81 |
dialog(opts) {
|
| 82 |
return new Promise(resolve => {
|
| 83 |
this._dialogResolver = resolve;
|
|
|
|
| 157 |
hideTos() { document.getElementById('tos-modal').style.display = 'none'; },
|
| 158 |
async acceptTos() { await fetch('/api/tos/accept', {method:'POST'}); this.user.tosAccepted = true; this.hideTos(); this.toast('CGU ✓', 'success'); },
|
| 159 |
|
| 160 |
+
// ---------- AVATARS ----------
|
| 161 |
loadAvatar() {
|
| 162 |
if (!this.user) return;
|
| 163 |
const url = '/api/users/' + this.user.id + '/avatar?' + Date.now();
|
|
|
|
| 189 |
this.loadAvatar();
|
| 190 |
},
|
| 191 |
|
| 192 |
+
// ---------- SETTINGS ----------
|
| 193 |
openSettings() {
|
| 194 |
document.getElementById('settings-modal').style.display = 'flex';
|
| 195 |
document.getElementById('set-lang').value = getLang();
|
|
|
|
| 315 |
el.addEventListener('touchmove', () => clearTimeout(t2));
|
| 316 |
},
|
| 317 |
|
| 318 |
+
// ---------- PARTAGÉS ----------
|
| 319 |
async loadSharedPage() {
|
| 320 |
const area = document.getElementById('file-area');
|
| 321 |
area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
|
|
|
|
| 361 |
const list = document.createElement('div'); list.className = 'share-list';
|
| 362 |
myShares.forEach(sh => {
|
| 363 |
const name = sh.file_name || sh.folder_name || '—';
|
|
|
|
| 364 |
const item = document.createElement('div'); item.className = 'share-item';
|
| 365 |
item.innerHTML =
|
| 366 |
'<div class="share-item-header"><div class="share-item-info"><svg class="icon icon-sm"><use href="#' + (sh.file_id ? 'icon-file' : 'icon-folder') + '"></use></svg><div style="min-width:0;"><div class="share-item-name">' + this.esc(name) + '</div><div class="share-item-sub">' + (sh.file_id ? 'Fichier' : 'Dossier') + ' · ' + t('shared_with') + ' <strong style="color:var(--primary);">' + this.esc(sh.shared_with_name) + '</strong></div></div></div><button class="btn-icon myshare-revoke" data-id="' + sh.id + '"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
|
|
|
|
| 421 |
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 422 |
},
|
| 423 |
|
| 424 |
+
// ==========================================
|
| 425 |
+
// ✅ MULTI-UPLOAD AVEC PROGRESSION RÉELLE (XHR)
|
| 426 |
+
// ==========================================
|
| 427 |
async handleUpload(input) {
|
| 428 |
const files = Array.from(input.files);
|
| 429 |
input.value = '';
|
|
|
|
| 441 |
},
|
| 442 |
|
| 443 |
uploadOne(file, idx, total) {
|
| 444 |
+
return new Promise((resolve) => {
|
| 445 |
const t0 = performance.now();
|
| 446 |
+
let uploadedBytes = 0; // octets envoyés navigateur → serveur
|
| 447 |
+
let phase = 'upload'; // 'upload' puis 'process'
|
| 448 |
+
let overallPct = 0; // % global (0-50 envoi, 50-100 traitement)
|
| 449 |
+
|
| 450 |
+
const countEl = document.getElementById('upload-count');
|
| 451 |
+
const nameEl = document.getElementById('upload-filename');
|
| 452 |
+
const barEl = document.getElementById('upload-progress');
|
| 453 |
+
const pctEl = document.getElementById('upload-percent');
|
| 454 |
+
const speedEl = document.getElementById('upload-speed');
|
| 455 |
+
|
| 456 |
+
countEl.textContent = '(' + idx + '/' + total + ')';
|
| 457 |
+
nameEl.textContent = file.name;
|
| 458 |
+
barEl.style.width = '0%';
|
| 459 |
+
pctEl.textContent = '0%';
|
| 460 |
+
speedEl.textContent = '';
|
| 461 |
+
|
| 462 |
+
// ✅ Ligne de stats injectée (temps / vitesse / restant)
|
| 463 |
+
let statsEl = document.getElementById('upload-stats');
|
| 464 |
+
if (!statsEl) {
|
| 465 |
+
statsEl = document.createElement('div');
|
| 466 |
+
statsEl.id = 'upload-stats';
|
| 467 |
+
statsEl.className = 'upload-status';
|
| 468 |
+
statsEl.style.fontSize = '0.75rem';
|
| 469 |
+
statsEl.style.marginTop = '0.4rem';
|
| 470 |
+
document.getElementById('upload-overlay').appendChild(statsEl);
|
| 471 |
+
}
|
| 472 |
+
statsEl.textContent = '';
|
| 473 |
+
|
| 474 |
+
// ✅ Chronomètre + stats live (2x par seconde)
|
| 475 |
+
const ticker = setInterval(() => {
|
| 476 |
+
const elapsed = (performance.now() - t0) / 1000;
|
| 477 |
+
let speed = 0, eta = '—';
|
| 478 |
+
if (phase === 'upload') {
|
| 479 |
+
if (elapsed > 0.3 && uploadedBytes > 0) {
|
| 480 |
+
speed = (uploadedBytes / 1048576) / elapsed;
|
| 481 |
+
if (speed > 0.01) eta = '~' + this.fmtDur((file.size - uploadedBytes) / (uploadedBytes / elapsed));
|
| 482 |
+
}
|
| 483 |
+
} else {
|
| 484 |
+
speed = (file.size / 1048576) / Math.max(elapsed, 0.1);
|
| 485 |
+
if (overallPct > 52) eta = '~' + this.fmtDur(elapsed * (100 - overallPct) / overallPct);
|
| 486 |
+
}
|
| 487 |
+
const speedTxt = speed > 0 ? speed.toFixed(2) + ' MB/s' : '…';
|
| 488 |
+
speedEl.textContent = speedTxt;
|
| 489 |
+
statsEl.textContent = t('up_elapsed') + ': ' + this.fmtDur(elapsed) +
|
| 490 |
+
' · ' + t('up_speed') + ': ' + speedTxt +
|
| 491 |
+
' · ' + t('up_eta') + ': ' + eta +
|
| 492 |
+
' · ' + (phase === 'upload' ? t('up_phase_upload') : t('up_phase_process'));
|
| 493 |
+
}, 500);
|
| 494 |
+
|
| 495 |
+
const finish = (ok) => {
|
| 496 |
+
clearInterval(ticker);
|
| 497 |
+
const elapsed = (performance.now() - t0) / 1000;
|
| 498 |
+
const speed = (file.size / 1048576) / Math.max(elapsed, 0.1);
|
| 499 |
+
statsEl.textContent = t('up_done_in') + ' ' + this.fmtDur(elapsed) + ' · ' + speed.toFixed(2) + ' MB/s';
|
| 500 |
+
speedEl.textContent = speed.toFixed(2) + ' MB/s';
|
| 501 |
+
setTimeout(() => resolve(ok), 900);
|
| 502 |
+
};
|
| 503 |
+
|
| 504 |
+
// ✅ XHR = vraie progression d'upload (fetch ne la donne pas !)
|
| 505 |
+
const xhr = new XMLHttpRequest();
|
| 506 |
const fd = new FormData();
|
| 507 |
fd.append('file', file);
|
| 508 |
if (this.currentFolder) fd.append('folder_id', this.currentFolder.id);
|
| 509 |
+
|
| 510 |
+
xhr.open('POST', '/api/files/upload');
|
| 511 |
+
xhr.upload.onprogress = (e) => {
|
| 512 |
+
if (e.lengthComputable) {
|
| 513 |
+
uploadedBytes = e.loaded;
|
| 514 |
+
overallPct = (e.loaded / e.total) * 50; // phase 1 = 0→50%
|
| 515 |
+
barEl.style.width = overallPct + '%';
|
| 516 |
+
pctEl.textContent = Math.round(overallPct) + '%';
|
| 517 |
+
}
|
| 518 |
};
|
| 519 |
+
xhr.onload = () => {
|
| 520 |
+
if (xhr.status === 200) {
|
| 521 |
+
let d;
|
| 522 |
+
try { d = JSON.parse(xhr.responseText); } catch (e) { return finish(false); }
|
| 523 |
+
phase = 'process';
|
| 524 |
+
// Phase 2 = 50→100% (SSE serveur)
|
| 525 |
+
const es = new EventSource('/api/files/' + d.fileId + '/progress');
|
| 526 |
+
es.onmessage = (ev) => {
|
| 527 |
+
const data = JSON.parse(ev.data);
|
| 528 |
+
if (data.type === 'progress') {
|
| 529 |
+
overallPct = 50 + (data.progress / 2);
|
| 530 |
+
barEl.style.width = overallPct + '%';
|
| 531 |
+
pctEl.textContent = Math.round(overallPct) + '%';
|
| 532 |
+
}
|
| 533 |
+
if (data.type === 'done') { es.close(); barEl.style.width = '100%'; pctEl.textContent = '100%'; finish(true); }
|
| 534 |
+
if (data.type === 'error') { es.close(); this.toast('Erreur: ' + file.name, 'error'); finish(false); }
|
| 535 |
+
};
|
| 536 |
+
es.onerror = () => { es.close(); finish(false); };
|
| 537 |
+
} else {
|
| 538 |
+
this.toast('Erreur upload: ' + file.name, 'error');
|
| 539 |
+
finish(false);
|
| 540 |
+
}
|
| 541 |
+
};
|
| 542 |
+
xhr.onerror = () => { this.toast('Erreur réseau: ' + file.name, 'error'); finish(false); };
|
| 543 |
+
xhr.onabort = () => { this._uploadCancelled = true; finish(false); };
|
| 544 |
+
|
| 545 |
+
this.uploadController = { abort: () => xhr.abort() };
|
| 546 |
+
xhr.send(fd);
|
| 547 |
});
|
| 548 |
},
|
| 549 |
|