Spaces:
Paused
Paused
Update public/app.js
Browse files- public/app.js +96 -13
public/app.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
| 1 |
// ==========================================
|
| 2 |
-
// PIXELDRIVE v4.
|
| 3 |
// ==========================================
|
| 4 |
-
if (window.I18N) {
|
| 5 |
-
Object.assign(window.I18N.fr, { up_elapsed: 'Temps', up_speed: 'Vitesse', up_eta: 'Restant', up_done_in: 'Terminé en', up_phase_upload: 'Envoi au serveur', up_phase_process: 'Traitement' });
|
| 6 |
-
Object.assign(window.I18N.en, { up_elapsed: 'Time', up_speed: 'Speed', up_eta: 'Left', up_done_in: 'Done in', up_phase_upload: 'Uploading to server', up_phase_process: 'Processing' });
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
const app = {
|
| 10 |
user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
|
| 11 |
files: [], folders: [], uploadController: null, _uploadCancelled: false, _moveTarget: null,
|
|
@@ -35,6 +30,8 @@ const app = {
|
|
| 35 |
});
|
| 36 |
document.getElementById('login-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.login(); });
|
| 37 |
document.getElementById('reg-pass2').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.register(); });
|
|
|
|
|
|
|
| 38 |
},
|
| 39 |
|
| 40 |
esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); },
|
|
@@ -50,6 +47,15 @@ const app = {
|
|
| 50 |
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
|
| 51 |
},
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
_switchHTML(key, onclick) {
|
| 54 |
const s = t(key);
|
| 55 |
const i = s.indexOf('?');
|
|
@@ -130,6 +136,8 @@ const app = {
|
|
| 130 |
document.getElementById('app-screen').style.display = 'flex';
|
| 131 |
document.getElementById('user-display').textContent = this.user.username;
|
| 132 |
document.getElementById('footer-username').textContent = this.user.username;
|
|
|
|
|
|
|
| 133 |
this.loadAvatar();
|
| 134 |
this.updateQuotaUI();
|
| 135 |
},
|
|
@@ -177,8 +185,7 @@ const app = {
|
|
| 177 |
document.getElementById('set-lang').value = getLang();
|
| 178 |
document.getElementById('set-theme').value = localStorage.getItem('pd_theme') || 'dark';
|
| 179 |
document.getElementById('set-sort').value = this.sortMode;
|
| 180 |
-
|
| 181 |
-
this.updateQuotaUI();
|
| 182 |
this.loadAvatar();
|
| 183 |
},
|
| 184 |
closeSettings() { document.getElementById('settings-modal').style.display = 'none'; },
|
|
@@ -221,6 +228,7 @@ const app = {
|
|
| 221 |
|
| 222 |
toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.querySelector('.sidebar-overlay').classList.toggle('active'); },
|
| 223 |
navTo(section) {
|
|
|
|
| 224 |
this.currentSection = section;
|
| 225 |
this.currentFolder = null;
|
| 226 |
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
|
@@ -237,6 +245,7 @@ const app = {
|
|
| 237 |
refresh() {
|
| 238 |
if (this.currentSection === 'files') this.loadContent();
|
| 239 |
else if (this.currentSection === 'shared') this.currentFolder ? this.loadContent() : this.loadSharedPage();
|
|
|
|
| 240 |
else this.loadRecent();
|
| 241 |
},
|
| 242 |
setSort(m) { this.sortMode = m; this.refresh(); },
|
|
@@ -283,6 +292,74 @@ const app = {
|
|
| 283 |
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 284 |
},
|
| 285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
renderGrid(folders, files, mode) {
|
| 287 |
const area = document.getElementById('file-area');
|
| 288 |
area.innerHTML = '';
|
|
@@ -441,7 +518,7 @@ const app = {
|
|
| 441 |
if (result) okCount++; else errCount++;
|
| 442 |
if (this._uploadCancelled) { this._uploadCancelled = false; break; }
|
| 443 |
}
|
| 444 |
-
|
| 445 |
document.getElementById('upload-overlay').style.display = 'none';
|
| 446 |
if (this.currentSection === 'files') this.loadContent();
|
| 447 |
if (okCount) this.toast(okCount + ' ✓' + (errCount ? ', ' + errCount + ' ✗' : ''), okCount && !errCount ? 'success' : 'error');
|
|
@@ -551,7 +628,6 @@ const app = {
|
|
| 551 |
else this.toast('Erreur', 'error');
|
| 552 |
},
|
| 553 |
|
| 554 |
-
// ✅ RENOMMER
|
| 555 |
async renameItem(type, item) {
|
| 556 |
const name = await this.prompt({ title: t('ctx_rename'), placeholder: t('ctx_rename'), value: item.name || '', confirmText: t('ctx_rename') });
|
| 557 |
if (!name || name === item.name) return;
|
|
@@ -562,7 +638,6 @@ const app = {
|
|
| 562 |
else this.toast(d.error || 'Erreur', 'error');
|
| 563 |
},
|
| 564 |
|
| 565 |
-
// ✅ DÉPLACER
|
| 566 |
async openMove(type, item) {
|
| 567 |
this._moveTarget = { type, item };
|
| 568 |
const sel = document.getElementById('move-select');
|
|
@@ -603,7 +678,10 @@ const app = {
|
|
| 603 |
container.innerHTML = '';
|
| 604 |
document.getElementById('preview-modal').style.display = 'flex';
|
| 605 |
const url = '/api/files/' + file.id + '/download';
|
| 606 |
-
document.getElementById('preview-download-btn').onclick = () => {
|
|
|
|
|
|
|
|
|
|
| 607 |
if (file.mime && file.mime.startsWith('image/')) { const i = document.createElement('img'); i.src = url; i.id = 'preview-content'; container.appendChild(i); }
|
| 608 |
else if (file.mime && file.mime.startsWith('video/')) { const v = document.createElement('video'); v.src = url; v.controls = true; v.autoplay = true; v.playsInline = true; v.id = 'preview-content'; container.appendChild(v); }
|
| 609 |
else if (file.mime && file.mime.startsWith('audio/')) { const a = document.createElement('audio'); a.src = url; a.controls = true; container.appendChild(a); }
|
|
@@ -679,7 +757,11 @@ const app = {
|
|
| 679 |
document.getElementById('ctx-delete').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
|
| 680 |
|
| 681 |
document.getElementById('ctx-preview').onclick = () => { this.closeMenu(); this.previewFile(item); };
|
| 682 |
-
document.getElementById('ctx-download').onclick = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
document.getElementById('ctx-share').onclick = () => { this.closeMenu(); this.showShareModal(item); };
|
| 684 |
document.getElementById('ctx-share-user').onclick = () => { this.closeMenu(); this.shareWithUser(isMineFile ? 'file' : 'folder', item); };
|
| 685 |
document.getElementById('ctx-rename').onclick = () => { this.closeMenu(); this.renameItem(isMineFile ? 'file' : 'folder', item); };
|
|
@@ -698,6 +780,7 @@ const app = {
|
|
| 698 |
if (!ok) return;
|
| 699 |
await fetch('/api/' + (isMineFile ? 'files' : 'folders') + '/' + item.id, {method:'DELETE'});
|
| 700 |
this.toast(t('t_deleted'), 'success');
|
|
|
|
| 701 |
this.refresh();
|
| 702 |
};
|
| 703 |
},
|
|
|
|
| 1 |
// ==========================================
|
| 2 |
+
// PIXELDRIVE v4.7 - APP.JS (admin panel + quotas live)
|
| 3 |
// ==========================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const app = {
|
| 5 |
user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
|
| 6 |
files: [], folders: [], uploadController: null, _uploadCancelled: false, _moveTarget: null,
|
|
|
|
| 30 |
});
|
| 31 |
document.getElementById('login-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.login(); });
|
| 32 |
document.getElementById('reg-pass2').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.register(); });
|
| 33 |
+
// ✅ QUOTAS EN DIRECT : refresh auto toutes les 30s
|
| 34 |
+
setInterval(() => { if (this.user) this.refreshQuota(); }, 30000);
|
| 35 |
},
|
| 36 |
|
| 37 |
esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); },
|
|
|
|
| 47 |
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
|
| 48 |
},
|
| 49 |
|
| 50 |
+
// ✅ Refresh quota (après download / périodique)
|
| 51 |
+
async refreshQuota() {
|
| 52 |
+
try {
|
| 53 |
+
const me = await fetch('/api/me');
|
| 54 |
+
if (me.ok) { this.user = Object.assign(this.user || {}, await me.json()); this.updateQuotaUI(); }
|
| 55 |
+
} catch (e) {}
|
| 56 |
+
},
|
| 57 |
+
scheduleQuotaRefresh() { setTimeout(() => this.refreshQuota(), 3000); },
|
| 58 |
+
|
| 59 |
_switchHTML(key, onclick) {
|
| 60 |
const s = t(key);
|
| 61 |
const i = s.indexOf('?');
|
|
|
|
| 136 |
document.getElementById('app-screen').style.display = 'flex';
|
| 137 |
document.getElementById('user-display').textContent = this.user.username;
|
| 138 |
document.getElementById('footer-username').textContent = this.user.username;
|
| 139 |
+
// ✅ Item admin visible seulement si isAdmin
|
| 140 |
+
document.getElementById('nav-admin').style.display = this.user.isAdmin ? 'flex' : 'none';
|
| 141 |
this.loadAvatar();
|
| 142 |
this.updateQuotaUI();
|
| 143 |
},
|
|
|
|
| 185 |
document.getElementById('set-lang').value = getLang();
|
| 186 |
document.getElementById('set-theme').value = localStorage.getItem('pd_theme') || 'dark';
|
| 187 |
document.getElementById('set-sort').value = this.sortMode;
|
| 188 |
+
await this.refreshQuota();
|
|
|
|
| 189 |
this.loadAvatar();
|
| 190 |
},
|
| 191 |
closeSettings() { document.getElementById('settings-modal').style.display = 'none'; },
|
|
|
|
| 228 |
|
| 229 |
toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.querySelector('.sidebar-overlay').classList.toggle('active'); },
|
| 230 |
navTo(section) {
|
| 231 |
+
if (section === 'admin' && (!this.user || !this.user.isAdmin)) return;
|
| 232 |
this.currentSection = section;
|
| 233 |
this.currentFolder = null;
|
| 234 |
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
|
|
|
| 245 |
refresh() {
|
| 246 |
if (this.currentSection === 'files') this.loadContent();
|
| 247 |
else if (this.currentSection === 'shared') this.currentFolder ? this.loadContent() : this.loadSharedPage();
|
| 248 |
+
else if (this.currentSection === 'admin') this.loadAdmin();
|
| 249 |
else this.loadRecent();
|
| 250 |
},
|
| 251 |
setSort(m) { this.sortMode = m; this.refresh(); },
|
|
|
|
| 292 |
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 293 |
},
|
| 294 |
|
| 295 |
+
// ✅ PANEL ADMIN
|
| 296 |
+
async loadAdmin() {
|
| 297 |
+
const area = document.getElementById('file-area');
|
| 298 |
+
area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
|
| 299 |
+
document.getElementById('breadcrumb').innerHTML = '<span>' + t('nav_admin') + '</span>';
|
| 300 |
+
if (!this.user || !this.user.isAdmin) { area.innerHTML = '<div class="empty-state"><p>403</p></div>'; return; }
|
| 301 |
+
try {
|
| 302 |
+
const [stRes, usRes] = await Promise.all([fetch('/api/admin/stats'), fetch('/api/admin/users')]);
|
| 303 |
+
if (!stRes.ok || !usRes.ok) throw new Error();
|
| 304 |
+
const stats = await stRes.json();
|
| 305 |
+
const users = await usRes.json();
|
| 306 |
+
area.innerHTML = '';
|
| 307 |
+
|
| 308 |
+
const t1 = document.createElement('div'); t1.className = 'section-title'; t1.textContent = t('admin_stats'); area.appendChild(t1);
|
| 309 |
+
const grid = document.createElement('div'); grid.className = 'admin-stats';
|
| 310 |
+
[
|
| 311 |
+
[stats.users, t('stat_users')],
|
| 312 |
+
[stats.filesCount, t('stat_files')],
|
| 313 |
+
[this.formatSize(stats.storage), t('stat_storage')],
|
| 314 |
+
[this.formatSize(stats.transferToday), t('stat_transfer')],
|
| 315 |
+
[stats.shares, t('stat_shares')],
|
| 316 |
+
[stats.links, t('stat_links')],
|
| 317 |
+
].forEach(([v, l]) => {
|
| 318 |
+
const c = document.createElement('div'); c.className = 'stat-card';
|
| 319 |
+
c.innerHTML = '<div class="stat-value">' + v + '</div><div class="stat-label">' + l + '</div>';
|
| 320 |
+
grid.appendChild(c);
|
| 321 |
+
});
|
| 322 |
+
area.appendChild(grid);
|
| 323 |
+
|
| 324 |
+
const t2 = document.createElement('div'); t2.className = 'section-title'; t2.textContent = t('admin_users'); area.appendChild(t2);
|
| 325 |
+
const wrap = document.createElement('div'); wrap.className = 'admin-table-wrap';
|
| 326 |
+
let rows = '';
|
| 327 |
+
users.forEach(u => {
|
| 328 |
+
const self = u.id === this.user.id;
|
| 329 |
+
rows += '<tr>' +
|
| 330 |
+
'<td><strong>' + this.esc(u.username) + '</strong> ' + (self ? '<span style="color:var(--text-muted);font-size:0.7rem;">' + t('admin_you') + '</span>' : '') + '</td>' +
|
| 331 |
+
'<td>' + u.filesCount + '</td>' +
|
| 332 |
+
'<td>' + this.formatSize(u.storageUsed) + '</td>' +
|
| 333 |
+
'<td>' + this.formatSize(u.transferToday) + ' / ' + this.formatSize(this.user.transferMax) + '</td>' +
|
| 334 |
+
'<td><span class="role-badge' + (u.is_admin ? ' admin' : '') + '">' + (u.is_admin ? t('admin_role_admin') : t('admin_role_user')) + '</span></td>' +
|
| 335 |
+
'<td style="white-space:nowrap;">' + (!self ?
|
| 336 |
+
'<button class="btn-icon" style="width:32px;height:32px;" title="' + t('admin_toggle') + '" onclick="app.toggleAdminRole(' + u.id + ')"><svg class="icon icon-sm"><use href="#icon-shield"></use></svg></button> ' +
|
| 337 |
+
'<button class="btn-icon" style="width:32px;height:32px;color:var(--danger);" title="' + t('admin_delete') + '" onclick="app.adminDeleteUser(' + u.id + ')"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button>'
|
| 338 |
+
: '—') + '</td></tr>';
|
| 339 |
+
});
|
| 340 |
+
wrap.innerHTML = '<table class="admin-table"><thead><tr><th>' + t('admin_col_user') + '</th><th>' + t('admin_col_files') + '</th><th>' + t('admin_col_storage') + '</th><th>' + t('admin_col_transfer') + '</th><th>' + t('admin_col_role') + '</th><th>' + t('admin_col_actions') + '</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
| 341 |
+
area.appendChild(wrap);
|
| 342 |
+
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 343 |
+
},
|
| 344 |
+
|
| 345 |
+
async toggleAdminRole(id) {
|
| 346 |
+
const ok = await this.confirm({ title: t('admin_toggle'), message: t('admin_toggle') + ' ?', confirmText: 'OK' });
|
| 347 |
+
if (!ok) return;
|
| 348 |
+
const res = await fetch('/api/admin/users/' + id + '/toggle-admin', { method: 'POST' });
|
| 349 |
+
const d = await res.json();
|
| 350 |
+
this.toast(d.ok ? t('t_admin_toggled') : (d.error || 'Erreur'), d.ok ? 'success' : 'error');
|
| 351 |
+
if (d.ok) this.loadAdmin();
|
| 352 |
+
},
|
| 353 |
+
|
| 354 |
+
async adminDeleteUser(id) {
|
| 355 |
+
const ok = await this.confirm({ title: t('admin_delete'), message: t('dlg_admin_del_msg'), confirmText: t('ctx_delete'), danger: true });
|
| 356 |
+
if (!ok) return;
|
| 357 |
+
const res = await fetch('/api/admin/users/' + id, { method: 'DELETE' });
|
| 358 |
+
const d = await res.json();
|
| 359 |
+
this.toast(d.ok ? t('t_admin_deleted') : (d.error || 'Erreur'), d.ok ? 'success' : 'error');
|
| 360 |
+
if (d.ok) this.loadAdmin();
|
| 361 |
+
},
|
| 362 |
+
|
| 363 |
renderGrid(folders, files, mode) {
|
| 364 |
const area = document.getElementById('file-area');
|
| 365 |
area.innerHTML = '';
|
|
|
|
| 518 |
if (result) okCount++; else errCount++;
|
| 519 |
if (this._uploadCancelled) { this._uploadCancelled = false; break; }
|
| 520 |
}
|
| 521 |
+
await this.refreshQuota();
|
| 522 |
document.getElementById('upload-overlay').style.display = 'none';
|
| 523 |
if (this.currentSection === 'files') this.loadContent();
|
| 524 |
if (okCount) this.toast(okCount + ' ✓' + (errCount ? ', ' + errCount + ' ✗' : ''), okCount && !errCount ? 'success' : 'error');
|
|
|
|
| 628 |
else this.toast('Erreur', 'error');
|
| 629 |
},
|
| 630 |
|
|
|
|
| 631 |
async renameItem(type, item) {
|
| 632 |
const name = await this.prompt({ title: t('ctx_rename'), placeholder: t('ctx_rename'), value: item.name || '', confirmText: t('ctx_rename') });
|
| 633 |
if (!name || name === item.name) return;
|
|
|
|
| 638 |
else this.toast(d.error || 'Erreur', 'error');
|
| 639 |
},
|
| 640 |
|
|
|
|
| 641 |
async openMove(type, item) {
|
| 642 |
this._moveTarget = { type, item };
|
| 643 |
const sel = document.getElementById('move-select');
|
|
|
|
| 678 |
container.innerHTML = '';
|
| 679 |
document.getElementById('preview-modal').style.display = 'flex';
|
| 680 |
const url = '/api/files/' + file.id + '/download';
|
| 681 |
+
document.getElementById('preview-download-btn').onclick = () => {
|
| 682 |
+
const a = document.createElement('a'); a.href = url; a.download = file.name; a.click();
|
| 683 |
+
this.scheduleQuotaRefresh(); // ✅ le download compte dans le quota → refresh UI
|
| 684 |
+
};
|
| 685 |
if (file.mime && file.mime.startsWith('image/')) { const i = document.createElement('img'); i.src = url; i.id = 'preview-content'; container.appendChild(i); }
|
| 686 |
else if (file.mime && file.mime.startsWith('video/')) { const v = document.createElement('video'); v.src = url; v.controls = true; v.autoplay = true; v.playsInline = true; v.id = 'preview-content'; container.appendChild(v); }
|
| 687 |
else if (file.mime && file.mime.startsWith('audio/')) { const a = document.createElement('audio'); a.src = url; a.controls = true; container.appendChild(a); }
|
|
|
|
| 757 |
document.getElementById('ctx-delete').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
|
| 758 |
|
| 759 |
document.getElementById('ctx-preview').onclick = () => { this.closeMenu(); this.previewFile(item); };
|
| 760 |
+
document.getElementById('ctx-download').onclick = () => {
|
| 761 |
+
this.closeMenu();
|
| 762 |
+
const a = document.createElement('a'); a.href = '/api/files/' + item.id + '/download'; a.download = item.name; a.click();
|
| 763 |
+
this.scheduleQuotaRefresh(); // ✅ download compté → refresh UI
|
| 764 |
+
};
|
| 765 |
document.getElementById('ctx-share').onclick = () => { this.closeMenu(); this.showShareModal(item); };
|
| 766 |
document.getElementById('ctx-share-user').onclick = () => { this.closeMenu(); this.shareWithUser(isMineFile ? 'file' : 'folder', item); };
|
| 767 |
document.getElementById('ctx-rename').onclick = () => { this.closeMenu(); this.renameItem(isMineFile ? 'file' : 'folder', item); };
|
|
|
|
| 780 |
if (!ok) return;
|
| 781 |
await fetch('/api/' + (isMineFile ? 'files' : 'folders') + '/' + item.id, {method:'DELETE'});
|
| 782 |
this.toast(t('t_deleted'), 'success');
|
| 783 |
+
this.refreshQuota();
|
| 784 |
this.refresh();
|
| 785 |
};
|
| 786 |
},
|