NathMen12 commited on
Commit
70ef3d1
·
verified ·
1 Parent(s): 2b40e66

Update public/app.js

Browse files
Files changed (1) hide show
  1. public/app.js +96 -13
public/app.js CHANGED
@@ -1,11 +1,6 @@
1
  // ==========================================
2
- // PIXELDRIVE v4.6 - APP.JS (rename/move + quotas UI)
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); },
@@ -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
- try { const me = await fetch('/api/me'); if (me.ok) this.user = Object.assign(this.user || {}, await me.json()); } catch (e) {}
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
- try { const me = await fetch('/api/me'); if (me.ok) { this.user = Object.assign(this.user, await me.json()); this.updateQuotaUI(); } } catch (e) {}
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 = () => { const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); };
 
 
 
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 = () => { this.closeMenu(); const a = document.createElement('a'); a.href = '/api/files/' + item.id + '/download'; a.download = item.name; a.click(); };
 
 
 
 
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); },
 
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
  },