NathMen12 commited on
Commit
80df7bc
·
verified ·
1 Parent(s): 0a5b454

Update public/app.js

Browse files
Files changed (1) hide show
  1. public/app.js +391 -481
public/app.js CHANGED
@@ -1,510 +1,420 @@
1
- // ==========================================
2
- // PIXELDRIVE v4.0 - APP.JS
3
- // ==========================================
4
- const app = {
5
- user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
6
- files: [], folders: [], uploadController: null,
7
- isMobile: window.innerWidth <= 768, suppressClick: false,
8
- _dialogResolver: null, _dialogOpts: null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- async init() {
11
- try {
12
- const res = await fetch('/api/me');
13
- if (res.ok) {
14
- this.user = await res.json();
15
- this.showApp();
16
- if (!this.user.tosAccepted) this.showTos(true);
17
- this.loadContent();
18
- }
19
- } catch (e) { console.error(e); }
20
- document.addEventListener('click', (e) => { if (!document.getElementById('context-menu').contains(e.target)) this.closeMenu(); });
21
- document.getElementById('file-area').addEventListener('contextmenu', (e) => e.preventDefault());
22
- window.addEventListener('resize', () => {
23
- this.isMobile = window.innerWidth <= 768;
24
- if (!this.isMobile) { document.getElementById('sidebar').classList.remove('open'); document.querySelector('.sidebar-overlay').classList.remove('active'); }
25
- });
26
- document.getElementById('login-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.login(); });
27
- document.getElementById('reg-pass2').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.register(); });
28
- },
29
 
30
- // ---------- UTILS ----------
31
- esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); },
32
- 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'; },
33
- statusLabel(s) { return ({pending:'En attente', uploading:'Envoi...', processing:'Traitement...', ready:'Prêt', error:'Erreur'})[s] || s; },
34
- formatDate(ts) { return ts ? new Date(ts).toLocaleString('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; },
35
- toast(msg, type) {
36
- const el = document.createElement('div');
37
- el.className = 'toast ' + (type || 'info');
38
- el.textContent = msg;
39
- document.getElementById('toast-container').appendChild(el);
40
- setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
41
- },
42
 
43
- //DIALOGUES CUSTOM (zéro popup navigateur)
44
- dialog(opts) {
45
- return new Promise(resolve => {
46
- this._dialogResolver = resolve;
47
- this._dialogOpts = opts;
48
- document.getElementById('dialog-title').textContent = opts.title || '';
49
- const msg = document.getElementById('dialog-message');
50
- msg.textContent = opts.message || '';
51
- msg.style.display = opts.message ? 'block' : 'none';
52
- const wrap = document.getElementById('dialog-input-wrap');
53
- const input = document.getElementById('dialog-input');
54
- if (opts.type === 'prompt') {
55
- wrap.style.display = 'block';
56
- input.value = opts.value || '';
57
- input.placeholder = opts.placeholder || '';
58
- setTimeout(() => input.focus(), 60);
59
- } else {
60
- wrap.style.display = 'none';
61
- }
62
- input.onkeydown = (e) => { if (e.key === 'Enter') this._dialogClose(true); };
63
- const ok = document.getElementById('dialog-ok');
64
- ok.textContent = opts.confirmText || 'Confirmer';
65
- ok.style.background = opts.danger ? 'var(--danger)' : 'var(--primary)';
66
- document.getElementById('dialog-modal').style.display = 'flex';
67
- });
68
- },
69
- _dialogClose(okClicked) {
70
- document.getElementById('dialog-modal').style.display = 'none';
71
- const opts = this._dialogOpts || {};
72
- const value = document.getElementById('dialog-input').value.trim();
73
- const r = this._dialogResolver;
74
- this._dialogResolver = null;
75
- if (r) r(okClicked ? (opts.type === 'prompt' ? (value || null) : true) : null);
76
- },
77
- confirm(opts) { return this.dialog(Object.assign({ type: 'confirm' }, opts)); },
78
- prompt(opts) { return this.dialog(Object.assign({ type: 'prompt' }, opts)); },
79
 
80
- // ---------- AUTH ----------
81
- toggleAuth(m) { document.getElementById('login-form').style.display = m === 'login' ? 'block' : 'none'; document.getElementById('register-form').style.display = m === 'register' ? 'block' : 'none'; },
82
- async login() {
83
- const u = document.getElementById('login-user').value.trim(), p = document.getElementById('login-pass').value;
84
- if (!u || !p) return this.toast('Champs requis', 'error');
85
- try {
86
- const res = await fetch('/api/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({u, p})});
87
- const d = await res.json();
88
- if (d.ok) { this.user = d.user; this.showApp(); if (!this.user.tosAccepted) this.showTos(true); this.navTo('files'); }
89
- else this.toast(d.error || 'Erreur', 'error');
90
- } catch (e) { this.toast('Erreur réseau', 'error'); }
91
- },
92
- async register() {
93
- const u = document.getElementById('reg-user').value.trim(), p = document.getElementById('reg-pass').value, p2 = document.getElementById('reg-pass2').value, tos = document.getElementById('reg-tos').checked;
94
- if (!u || !p) return this.toast('Champs requis', 'error');
95
- if (p !== p2) return this.toast('Mots de passe différents', 'error');
96
- if (!tos) return this.toast('Acceptez les CGU', 'error');
97
- try {
98
- const res = await fetch('/api/register', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({u, p, t:true})});
99
- const d = await res.json();
100
- if (d.ok) { this.toast('Compte créé !', 'success'); this.toggleAuth('login'); } else this.toast(d.error, 'error');
101
- } catch (e) { this.toast('Erreur', 'error'); }
102
- },
103
- async logout() {
104
- const ok = await this.confirm({ title: 'Déconnexion', message: 'Voulez-vous vraiment vous déconnecter ?', confirmText: 'Déconnexion', danger: true });
105
- if (!ok) return;
106
- await fetch('/api/logout', {method:'POST'});
107
- location.reload();
108
- },
109
- showApp() { document.getElementById('auth-screen').style.display = 'none'; document.getElementById('app-screen').style.display = 'flex'; document.getElementById('user-display').textContent = this.user.username; },
110
 
111
- // ---------- CGU (iframe) ----------
112
- showTos(force) {
113
- document.getElementById('tos-modal').style.display = 'flex';
114
- document.getElementById('tos-accept-btn').style.display = force ? 'flex' : 'none';
115
- },
116
- hideTos() { document.getElementById('tos-modal').style.display = 'none'; },
117
- async acceptTos() { await fetch('/api/tos/accept', {method:'POST'}); this.user.tosAccepted = true; this.hideTos(); this.toast('CGU acceptées', 'success'); },
 
 
 
 
 
 
118
 
119
- // ---------- NAVIGATION ----------
120
- toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.querySelector('.sidebar-overlay').classList.toggle('active'); },
121
- navTo(section) {
122
- this.currentSection = section;
123
- this.currentFolder = null;
124
- document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
125
- document.getElementById('nav-' + section).classList.add('active');
126
- if (this.isMobile) this.toggleSidebar();
127
- document.getElementById('toolbar-actions').style.display = (section === 'files') ? 'flex' : 'none';
128
- this.refresh();
129
- },
130
- goRoot() {
131
- if (this.currentSection === 'shared') { this.currentFolder = null; this.loadSharedPage(); return; }
132
- this.currentFolder = null;
133
- this.loadContent();
134
- },
135
- refresh() {
136
- if (this.currentSection === 'files') this.loadContent();
137
- else if (this.currentSection === 'shared') this.currentFolder ? this.loadContent() : this.loadSharedPage();
138
- else this.loadRecent();
139
- },
140
- setSort(m) { this.sortMode = m; this.refresh(); },
141
- sortFiles(files) {
142
- const a = [...files];
143
- if (this.sortMode === 'name-asc') a.sort((x,y) => x.name.localeCompare(y.name, undefined, {sensitivity:'base'}));
144
- else if (this.sortMode === 'name-desc') a.sort((x,y) => y.name.localeCompare(x.name, undefined, {sensitivity:'base'}));
145
- else if (this.sortMode === 'size-desc') a.sort((x,y) => (y.size||0) - (x.size||0));
146
- else a.sort((x,y) => (y.updated_at||y.created_at||0) - (x.updated_at||x.created_at||0));
147
- return a;
148
- },
149
 
150
- // ---------- FILES ----------
151
- async loadContent() {
152
- const area = document.getElementById('file-area');
153
- area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">Chargement...</div>';
154
- const bc = document.getElementById('breadcrumb');
155
- bc.innerHTML = (this.currentSection === 'shared' ? '<span onclick="app.goRoot()">Partagés</span>' : '<span onclick="app.goRoot()">Mes Fichiers</span>') +
156
- (this.currentFolder ? ' <span class="sep">/</span> <span>' + this.esc(this.currentFolder.name) + '</span>' : '');
157
- try {
158
- const [fRes, dRes] = await Promise.all([
159
- fetch('/api/files?folder_id=' + (this.currentFolder ? this.currentFolder.id : '')),
160
- fetch('/api/folders?parent_id=' + (this.currentFolder ? this.currentFolder.id : ''))
161
- ]);
162
- this.files = await fRes.json();
163
- this.folders = await dRes.json();
164
- this.renderGrid(this.folders, this.files, this.currentSection === 'shared' ? 'shared' : 'mine');
165
- } catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur de chargement</div>'; }
166
- },
167
 
168
- async loadRecent() {
169
- const area = document.getElementById('file-area');
170
- area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">Chargement...</div>';
171
- document.getElementById('breadcrumb').innerHTML = '<span>Récents</span>';
172
- try {
173
- const all = [];
174
- const walk = async (pid) => {
175
- const fr = await fetch('/api/files?folder_id=' + (pid || ''));
176
- (await fr.json()).forEach(f => all.push(f));
177
- const dr = await fetch('/api/folders?parent_id=' + (pid || ''));
178
- for (const d of await dr.json()) await walk(d.id);
179
- };
180
- await walk(null);
181
- this.renderGrid([], this.sortFiles(all).slice(0, 20), 'mine');
182
- } catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
183
- },
184
 
185
- renderGrid(folders, files, mode) {
186
- const area = document.getElementById('file-area');
187
- area.innerHTML = '';
188
- if (!folders.length && !files.length) {
189
- area.innerHTML = '<div class="empty-state"><svg class="icon icon-lg"><use href="#icon-folder"></use></svg><p>Dossier vide</p></div>';
190
- return;
191
- }
192
- const grid = document.createElement('div');
193
- grid.className = 'grid';
194
 
195
- folders.forEach(f => {
196
- const el = document.createElement('div');
197
- el.className = 'card';
198
- el.innerHTML = '<div class="card-thumb folder-thumb">' + (f.owner_name ? '<span class="owner-badge">' + this.esc(f.owner_name) + '</span>' : '') + '<svg class="icon"><use href="#icon-folder"></use></svg></div><div class="card-info"><div class="card-name">' + this.esc(f.name) + '</div><div class="card-meta"><span>Dossier</span><span></span></div></div>';
199
- el.onclick = () => { if (this.suppressClick) { this.suppressClick = false; return; } this.currentFolder = f; this.loadContent(); };
200
- el.oncontextmenu = (e) => this.showContextMenu(e, mode === 'shared' ? 'shared-folder' : 'folder', f);
201
- this.bindLongPress(el, mode === 'shared' ? 'shared-folder' : 'folder', f);
202
- grid.appendChild(el);
203
- });
204
 
205
- this.sortFiles(files).forEach(f => {
206
- const el = document.createElement('div');
207
- el.className = 'card';
208
- let thumb = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
209
- if (f.mime && (f.mime.startsWith('image/') || f.mime.startsWith('video/'))) thumb = '<img src="/api/files/' + f.id + '/thumb" alt="" loading="lazy">';
210
- el.innerHTML = '<div class="card-thumb">' + (f.owner_name ? '<span class="owner-badge">' + this.esc(f.owner_name) + '</span>' : '') + thumb + '</div><div class="card-info"><div class="card-name">' + this.esc(f.name) + '</div><div class="card-meta"><span>' + this.formatSize(f.size) + '</span><span>' + this.statusLabel(f.status) + '</span></div></div>';
211
- el.onclick = () => { if (this.suppressClick) { this.suppressClick = false; return; } if (f.status === 'ready') this.previewFile(f); };
212
- el.oncontextmenu = (e) => this.showContextMenu(e, mode === 'shared' ? 'shared-file' : 'file', f);
213
- this.bindLongPress(el, mode === 'shared' ? 'shared-file' : 'file', f);
214
- grid.appendChild(el);
215
- });
216
- area.appendChild(grid);
217
- },
218
 
219
- bindLongPress(el, type, item) {
220
- let t = null;
221
- el.addEventListener('touchstart', (e) => { t = setTimeout(() => { this.suppressClick = true; this.showContextMenu(e, type, item); }, 500); }, {passive:true});
222
- el.addEventListener('touchend', () => clearTimeout(t));
223
- el.addEventListener('touchmove', () => clearTimeout(t));
224
- },
225
 
226
- // ONGLET PARTAGÉS : reçus + gestion propriétaire + liens publics
227
- async loadSharedPage() {
228
- const area = document.getElementById('file-area');
229
- area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">Chargement...</div>';
230
- document.getElementById('breadcrumb').innerHTML = '<span>Partagés</span>';
231
- try {
232
- const [shRes, myRes, lkRes] = await Promise.all([fetch('/api/shared-with-me'), fetch('/api/my-shares'), fetch('/api/share-links')]);
233
- const shared = await shRes.json();
234
- const myShares = await myRes.json();
235
- const links = await lkRes.json();
236
- area.innerHTML = '';
237
 
238
- // --- Section 1 : reçus ---
239
- const t1 = document.createElement('div'); t1.className = 'section-title'; t1.textContent = 'Partagés avec moi'; area.appendChild(t1);
240
- if (!shared.length) {
241
- area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:2rem 1rem;"><svg class="icon icon-lg"><use href="#icon-share"></use></svg><p>Rien ne vous a été partagé</p></div>');
242
- } else {
243
- const grid = document.createElement('div'); grid.className = 'grid';
244
- shared.forEach(item => {
245
- const el = document.createElement('div'); el.className = 'card';
246
- if (item.folder_id) {
247
- el.innerHTML = '<div class="card-thumb folder-thumb"><span class="owner-badge">' + this.esc(item.owner_name) + '</span><svg class="icon"><use href="#icon-folder"></use></svg></div><div class="card-info"><div class="card-name">' + this.esc(item.folder_name) + '</div><div class="card-meta"><span>Dossier</span><span></span></div></div>';
248
- el.onclick = () => { this.currentFolder = {id: item.folder_id, name: item.folder_name}; this.loadContent(); };
249
- el.oncontextmenu = (e) => this.showContextMenu(e, 'shared-folder', item);
250
- this.bindLongPress(el, 'shared-folder', item);
251
- } else {
252
- let thumb = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
253
- if (item.mime && (item.mime.startsWith('image/') || item.mime.startsWith('video/'))) thumb = '<img src="/api/files/' + item.file_id + '/thumb" alt="" loading="lazy">';
254
- const fobj = Object.assign({share_id: item.share_id}, {id: item.file_id, name: item.file_name, mime: item.mime, size: item.size, status: item.status});
255
- el.innerHTML = '<div class="card-thumb"><span class="owner-badge">' + this.esc(item.owner_name) + '</span>' + thumb + '</div><div class="card-info"><div class="card-name">' + this.esc(item.file_name || 'Fichier supprimé') + '</div><div class="card-meta"><span>' + this.formatSize(item.size) + '</span><span>' + this.statusLabel(item.status) + '</span></div></div>';
256
- el.onclick = () => { if (item.status === 'ready') this.previewFile(fobj); };
257
- el.oncontextmenu = (e) => this.showContextMenu(e, 'shared-file', fobj);
258
- this.bindLongPress(el, 'shared-file', fobj);
259
- }
260
- grid.appendChild(el);
261
- });
262
- area.appendChild(grid);
263
- }
264
 
265
- // --- Section 2 : gestion de MES partages utilisateurs ---
266
- const t2 = document.createElement('div'); t2.className = 'section-title'; t2.textContent = 'Mes partages utilisateurs (gestion)'; area.appendChild(t2);
267
- if (!myShares.length) {
268
- area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:1.5rem 1rem;"><p>Aucun partage actif</p><p style="font-size:0.8rem;margin-top:0.4rem;">Clic droit sur un fichier/dossier → Partager à un utilisateur</p></div>');
269
- } else {
270
- const list = document.createElement('div'); list.className = 'share-list';
271
- myShares.forEach(sh => {
272
- const name = sh.file_name || sh.folder_name || 'Élément supprimé';
273
- const kind = sh.file_id ? 'Fichier' : 'Dossier';
274
- const item = document.createElement('div'); item.className = 'share-item';
275
- item.innerHTML =
276
- '<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">' + kind + ' → partagé avec <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 + '" title="Révoquer"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
277
- '<div class="share-item-footer">Partagé le ' + this.formatDate(sh.created_at) + '</div>';
278
- list.appendChild(item);
279
- });
280
- area.appendChild(list);
281
- area.querySelectorAll('.myshare-revoke').forEach(btn => {
282
- btn.onclick = async () => {
283
- const ok = await this.confirm({ title: 'Révoquer le partage', message: 'Cette personne perdra immédiatement l\'accès.', confirmText: 'Révoquer', danger: true });
284
- if (!ok) return;
285
- await fetch('/api/my-shares/' + btn.dataset.id, {method:'DELETE'});
286
- this.toast('Partage révoqué', 'success');
287
- this.loadSharedPage();
288
- };
289
- });
290
- }
291
 
292
- // --- Section 3 : liens publics ---
293
- const t3 = document.createElement('div'); t3.className = 'section-title'; t3.textContent = 'Mes liens publics'; area.appendChild(t3);
294
- if (!links.length) {
295
- area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:1.5rem 1rem;"><p>Aucun lien public</p></div>');
296
- } else {
297
- const grouped = {};
298
- links.forEach(l => { if (!grouped[l.file_id]) grouped[l.file_id] = {name: l.file_name, links: []}; grouped[l.file_id].links.push(l); });
299
- const list = document.createElement('div'); list.className = 'share-list';
300
- Object.entries(grouped).forEach(([fid, data]) => {
301
- const emb = data.links.find(l => l.type === 'embed');
302
- const dl = data.links.find(l => l.type === 'download');
303
- const exp = this.formatDate(Math.max(...data.links.map(l => l.expires_at || 0)));
304
- const origin = window.location.origin;
305
- const item = document.createElement('div'); item.className = 'share-item';
306
- item.innerHTML =
307
- '<div class="share-item-header"><div class="share-item-info"><svg class="icon icon-sm"><use href="#icon-file"></use></svg><span class="share-item-name">' + this.esc(data.name || 'Fichier supprimé') + '</span></div><button class="btn-icon link-del-btn" data-fid="' + fid + '"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
308
- '<div class="share-item-links">' +
309
- (emb ? '<div class="share-link-row"><span class="share-link-label">Aperçu</span><input type="text" readonly class="form-input share-link-input" value="' + origin + '/s/' + emb.token + '"><button class="btn-icon share-copy-btn" data-url="' + origin + '/s/' + emb.token + '"><svg class="icon icon-sm"><use href="#icon-link"></use></svg></button></div>' : '') +
310
- (dl ? '<div class="share-link-row"><span class="share-link-label">Download</span><input type="text" readonly class="form-input share-link-input" value="' + origin + '/d/' + dl.token + '"><button class="btn-icon share-copy-btn" data-url="' + origin + '/d/' + dl.token + '"><svg class="icon icon-sm"><use href="#icon-link"></use></svg></button></div>' : '') +
311
- '</div><div class="share-item-footer">Expire le ' + exp + '</div>';
312
- list.appendChild(item);
313
- });
314
- area.appendChild(list);
315
- area.querySelectorAll('.share-copy-btn').forEach(btn => {
316
- btn.onclick = async () => {
317
- try { await navigator.clipboard.writeText(btn.dataset.url); } catch (e) { btn.previousElementSibling.select(); document.execCommand('copy'); }
318
- btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500);
319
- this.toast('Lien copié !', 'success');
320
- };
321
- });
322
- area.querySelectorAll('.link-del-btn').forEach(btn => {
323
- btn.onclick = async () => {
324
- const ok = await this.confirm({ title: 'Supprimer les liens', message: 'Tous les liens publics de ce fichier seront révoqués.', confirmText: 'Supprimer', danger: true });
325
- if (!ok) return;
326
- for (const t of grouped[btn.dataset.fid].links.map(l => l.token)) await fetch('/api/share-links/' + t, {method:'DELETE'});
327
- this.toast('Liens supprimés', 'success');
328
- this.loadSharedPage();
329
- };
330
- });
331
- }
332
- } catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
333
- },
334
 
335
- // ✅ MULTI-UPLOAD avec vitesse
336
- async handleUpload(input) {
337
- const files = Array.from(input.files);
338
- input.value = '';
339
- if (!files.length) return;
340
- const overlay = document.getElementById('upload-overlay');
341
- overlay.style.display = 'block';
342
- let okCount = 0, errCount = 0;
343
- for (let i = 0; i < files.length; i++) {
344
- const result = await this.uploadOne(files[i], i + 1, files.length);
345
- if (result) okCount++; else errCount++;
346
- if (this._uploadCancelled) { this._uploadCancelled = false; break; }
347
  }
348
- overlay.style.display = 'none';
349
- if (this.currentSection === 'files') this.loadContent();
350
- if (okCount) this.toast(okCount + ' fichier(s) uploadé(s)' + (errCount ? ', ' + errCount + ' erreur(s)' : ''), okCount && !errCount ? 'success' : 'error');
351
- },
352
-
353
- uploadOne(file, idx, total) {
354
- return new Promise(async (resolve) => {
355
- const t0 = performance.now();
356
- document.getElementById('upload-count').textContent = '(' + idx + '/' + total + ')';
357
- document.getElementById('upload-filename').textContent = file.name;
358
- document.getElementById('upload-progress').style.width = '0%';
359
- document.getElementById('upload-percent').textContent = '0%';
360
- document.getElementById('upload-speed').textContent = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
- const fd = new FormData();
363
- fd.append('file', file);
364
- if (this.currentFolder) fd.append('folder_id', this.currentFolder.id);
365
- this.uploadController = new AbortController();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
- const finish = (ok) => {
368
- const secs = (performance.now() - t0) / 1000;
369
- const mbps = (file.size / 1048576) / secs;
370
- if (ok && isFinite(mbps)) document.getElementById('upload-speed').textContent = mbps.toFixed(2) + ' MB/s';
371
- setTimeout(() => resolve(ok), 600);
372
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
- try {
375
- const res = await fetch('/api/files/upload', {method:'POST', body: fd, signal: this.uploadController.signal});
376
- if (!res.ok) throw new Error('fail');
377
- const d = await res.json();
378
- const es = new EventSource('/api/files/' + d.fileId + '/progress');
379
- es.onmessage = (e) => {
380
- const data = JSON.parse(e.data);
381
- if (data.type === 'progress') {
382
- document.getElementById('upload-progress').style.width = Math.round(data.progress) + '%';
383
- document.getElementById('upload-percent').textContent = Math.round(data.progress) + '%';
384
- const secs = (performance.now() - t0) / 1000;
385
- if (secs > 0.5) document.getElementById('upload-speed').textContent = ((file.size / 1048576) / secs).toFixed(2) + ' MB/s';
386
- }
387
- if (data.type === 'done') { es.close(); finish(true); }
388
- if (data.type === 'error') { es.close(); this.toast('Erreur: ' + file.name, 'error'); finish(false); }
389
- };
390
- es.onerror = () => { es.close(); finish(false); };
391
- } catch (e) {
392
- if (e.name === 'AbortError') { this._uploadCancelled = true; resolve(false); }
393
- else { this.toast('Erreur upload: ' + file.name, 'error'); finish(false); }
394
- }
395
- });
396
- },
397
 
398
- cancelUpload() { if (this.uploadController) this.uploadController.abort(); document.getElementById('upload-overlay').style.display = 'none'; },
 
 
 
 
 
 
 
 
399
 
400
- async createFolder() {
401
- const name = await this.prompt({ title: 'Nouveau dossier', placeholder: 'Nom du dossier', confirmText: 'Créer' });
402
- if (!name) return;
403
- const res = await fetch('/api/folders', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({n: name, p: this.currentFolder ? this.currentFolder.id : null})});
404
- if (res.ok) { this.toast('Dossier créé', 'success'); this.loadContent(); }
405
- else this.toast('Erreur création', 'error');
406
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
- previewFile(file) {
409
- const container = document.getElementById('preview-container');
410
- container.innerHTML = '';
411
- document.getElementById('preview-modal').style.display = 'flex';
412
- const url = '/api/files/' + file.id + '/download';
413
- document.getElementById('preview-download-btn').onclick = () => { const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); };
414
- if (file.mime && file.mime.startsWith('image/')) { const i = document.createElement('img'); i.src = url; i.id = 'preview-content'; container.appendChild(i); }
415
- 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); }
416
- else if (file.mime && file.mime.startsWith('audio/')) { const a = document.createElement('audio'); a.src = url; a.controls = true; container.appendChild(a); }
417
- else container.innerHTML = '<div style="text-align:center;padding:2rem;"><p style="color:var(--text-muted);">Aperçu non disponible</p><p style="font-weight:500;margin-top:0.5rem;">' + this.esc(file.name) + '</p></div>';
418
- },
419
- closePreview() {
420
- const c = document.getElementById('preview-container');
421
- const v = c.querySelector('video'); const a = c.querySelector('audio');
422
- if (v) v.pause(); if (a) a.pause();
423
- c.innerHTML = '';
424
- document.getElementById('preview-modal').style.display = 'none';
425
- },
426
 
427
- async showShareModal(file) {
428
- document.getElementById('share-modal').style.display = 'flex';
429
- const pi = document.getElementById('share-preview-url'), di = document.getElementById('share-download-url'), ei = document.getElementById('share-embed-code');
430
- pi.value = di.value = ei.value = 'Génération...';
431
- try {
432
- const res = await fetch('/api/files/' + file.id + '/share', {method:'POST'});
433
- if (!res.ok) throw new Error();
434
- const d = await res.json();
435
- const o = window.location.origin;
436
- pi.value = o + d.preview;
437
- di.value = o + d.download;
438
- ei.value = file.mime.startsWith('image/') ? '<img src="' + o + d.download + '" alt="">' :
439
- file.mime.startsWith('video/') ? '<video controls src="' + o + d.download + '"></video>' :
440
- file.mime.startsWith('audio/') ? '<audio controls src="' + o + d.download + '"></audio>' :
441
- '<a href="' + o + d.download + '">Télécharger ' + this.esc(file.name) + '</a>';
442
- document.querySelectorAll('.share-copy-btn').forEach(b => { b.onclick = () => this.copyShare(b.dataset.type); });
443
- } catch (e) { this.toast('Erreur génération des liens', 'error'); this.closeShare(); }
444
- },
445
- closeShare() { document.getElementById('share-modal').style.display = 'none'; },
446
- async copyShare(type) {
447
- const id = type === 'preview' ? 'share-preview-url' : type === 'download' ? 'share-download-url' : 'share-embed-code';
448
- const input = document.getElementById(id);
449
- if (!input.value || input.value === 'Génération...') return;
450
- try { await navigator.clipboard.writeText(input.value); } catch (e) { input.select(); document.execCommand('copy'); }
451
- const btn = document.querySelector('.share-copy-btn[data-type="' + type + '"]');
452
- if (btn) { btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }
453
- this.toast('Lien copié !', 'success');
454
- },
455
 
456
- async shareWithUser(type, item) {
457
- const username = await this.prompt({ title: 'Partager à un utilisateur', placeholder: "Nom d'utilisateur exact", confirmText: 'Partager' });
458
- if (!username) return;
459
- const res = await fetch('/api/share-user', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({type, id: item.id, username})});
460
- const d = await res.json();
461
- this.toast(d.ok ? 'Partagé avec ' + username : (d.error || 'Erreur'), d.ok ? 'success' : 'error');
462
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
- showContextMenu(e, type, item) {
465
- try { e.preventDefault(); } catch (err) {}
466
- e.stopPropagation();
467
- const menu = document.getElementById('context-menu');
468
- menu.style.display = 'block';
469
- if (this.isMobile) { menu.style.left = '0'; menu.style.right = '0'; menu.style.bottom = '0'; menu.style.top = 'auto'; menu.style.width = '100%'; }
470
- else {
471
- const cx = e.pageX || 0, cy = e.pageY || 0;
472
- menu.style.left = cx + 'px'; menu.style.top = cy + 'px'; menu.style.bottom = 'auto'; menu.style.right = 'auto'; menu.style.width = 'auto';
473
- const r = menu.getBoundingClientRect();
474
- if (r.right > window.innerWidth) menu.style.left = (cx - r.width) + 'px';
475
- if (r.bottom > window.innerHeight) menu.style.top = (cy - r.height) + 'px';
476
- }
477
- const isMineFile = type === 'file', isMineFolder = type === 'folder';
478
- const isSharedFile = type === 'shared-file', isSharedFolder = type === 'shared-folder';
479
- document.getElementById('ctx-preview').style.display = (isMineFile || isSharedFile) ? 'flex' : 'none';
480
- document.getElementById('ctx-download').style.display = (isMineFile || isSharedFile) ? 'flex' : 'none';
481
- document.getElementById('ctx-share').style.display = isMineFile ? 'flex' : 'none';
482
- document.getElementById('ctx-share-user').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
483
- document.getElementById('ctx-remove').style.display = (isSharedFile || isSharedFolder) ? 'flex' : 'none';
484
- document.getElementById('ctx-delete').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
485
 
486
- document.getElementById('ctx-preview').onclick = () => { this.closeMenu(); this.previewFile(item); };
487
- 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(); };
488
- document.getElementById('ctx-share').onclick = () => { this.closeMenu(); this.showShareModal(item); };
489
- document.getElementById('ctx-share-user').onclick = () => { this.closeMenu(); this.shareWithUser(isMineFile ? 'file' : 'folder', item); };
490
- document.getElementById('ctx-remove').onclick = async () => {
491
- this.closeMenu();
492
- const ok = await this.confirm({ title: 'Retirer de ma liste', message: 'Vous perdrez l\'accès à cet élément partagé.', confirmText: 'Retirer', danger: true });
493
- if (!ok) return;
494
- await fetch('/api/shared-with-me/' + item.share_id, {method:'DELETE'});
495
- this.toast('Retiré', 'success');
496
- this.loadSharedPage();
497
- };
498
- document.getElementById('ctx-delete').onclick = async () => {
499
- this.closeMenu();
500
- const ok = await this.confirm({ title: 'Supprimer', message: 'Supprimer définitivement "' + (item.name || '') + '" ?', confirmText: 'Supprimer', danger: true });
501
- if (!ok) return;
502
- await fetch('/api/' + (isMineFile ? 'files' : 'folders') + '/' + item.id, {method:'DELETE'});
503
- this.toast('Supprimé', 'success');
504
- this.refresh();
505
- };
506
- },
507
- closeMenu() { document.getElementById('context-menu').style.display = 'none'; }
508
- };
509
 
510
- app.init();
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>PixelDrive</title>
7
+ <link rel="manifest" href="/manifest.json">
8
+ <meta name="theme-color" content="#4f46e5">
9
+ <!-- ✅ OPEN GRAPH : bannière visible dans WhatsApp/Messages/Discord, PAS sur le site -->
10
+ <meta property="og:type" content="website">
11
+ <meta property="og:site_name" content="PixelDrive">
12
+ <meta property="og:title" content="PixelDrive — Stockage cloud chiffré Zero-Knowledge">
13
+ <meta property="og:description" content="Importez, chiffrez (AES-256-GCM) et partagez vos fichiers en toute confidentialité.">
14
+ <meta property="og:image" content="https://nathmen12-pixel-drive.hf.space/banner.png">
15
+ <meta property="og:url" content="https://nathmen12-pixel-drive.hf.space/">
16
+ <meta name="twitter:card" content="summary_large_image">
17
+ <meta name="twitter:title" content="PixelDrive — Stockage cloud chiffré">
18
+ <meta name="twitter:description" content="Partage sécurisé de fichiers chiffrés.">
19
+ <meta name="twitter:image" content="https://nathmen12-pixel-drive.hf.space/banner.png">
20
+ <link rel="icon" type="image/png" href="/banner.png">
21
+ <style>
22
+ :root { --bg-dark:#0f0f13; --bg-panel:#18181b; --bg-hover:#27272a; --primary:#6366f1; --primary-hover:#4f46e5; --text-main:#f4f4f5; --text-muted:#a1a1aa; --border:#27272a; --danger:#ef4444; --success:#22c55e; --radius:8px; --font:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; --sidebar-width:260px; }
23
+ * { box-sizing:border-box; margin:0; padding:0; outline:none; -webkit-tap-highlight-color:transparent; }
24
+ body { font-family:var(--font); background:var(--bg-dark); color:var(--text-main); height:100dvh; overflow:hidden; display:flex; flex-direction:column; }
25
+ .icon { width:20px; height:20px; stroke:currentColor; stroke-width:2; fill:none; stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
26
+ .icon-sm { width:16px; height:16px; }
27
+ .icon-lg { width:48px; height:48px; stroke-width:1.5; }
28
 
29
+ #auth-screen { position:fixed; inset:0; background:var(--bg-dark); z-index:1000; display:flex; align-items:center; justify-content:center; padding:1rem; }
30
+ .auth-card { background:var(--bg-panel); padding:2rem; border-radius:var(--radius); width:100%; max-width:400px; border:1px solid var(--border); }
31
+ .auth-header { text-align:center; margin-bottom:2rem; }
32
+ .auth-header h1 { font-size:1.5rem; font-weight:700; color:var(--primary); display:flex; align-items:center; justify-content:center; gap:0.5rem; }
33
+ .form-group { margin-bottom:1rem; }
34
+ .form-group label { display:block; font-size:0.875rem; color:var(--text-muted); margin-bottom:0.5rem; }
35
+ .form-input { width:100%; padding:0.875rem; background:var(--bg-dark); border:1px solid var(--border); border-radius:var(--radius); color:var(--text-main); font-size:1rem; }
36
+ .form-input:focus { border-color:var(--primary); }
37
+ .btn { width:100%; padding:0.875rem; background:var(--primary); color:white; border:none; border-radius:var(--radius); font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:0.5rem; font-size:1rem; }
38
+ .btn-secondary { background:transparent; border:1px solid var(--border); color:var(--text-muted); }
39
+ .auth-switch { margin-top:1.5rem; text-align:center; font-size:0.875rem; color:var(--text-muted); }
40
+ .auth-switch a { color:var(--primary); cursor:pointer; font-weight:500; }
 
 
 
 
 
 
 
41
 
42
+ #app-screen { display:none; height:100%; flex-direction:column; }
43
+ header { height:60px; background:var(--bg-panel); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; padding:0 1rem; z-index:100; flex-shrink:0; }
44
+ .header-left { display:flex; align-items:center; gap:0.75rem; }
45
+ .menu-btn { display:none; background:transparent; border:none; color:var(--text-main); cursor:pointer; padding:0.5rem; border-radius:var(--radius); }
46
+ .logo { font-weight:700; font-size:1.25rem; color:var(--primary); display:flex; align-items:center; gap:0.5rem; }
47
+ .user-menu { display:flex; align-items:center; gap:0.5rem; }
48
+ .user-badge { font-size:0.875rem; color:var(--text-muted); }
49
+ .main-container { display:flex; flex:1; overflow:hidden; }
50
+ aside { width:var(--sidebar-width); background:var(--bg-panel); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:1rem; flex-shrink:0; transition:transform 0.3s ease; }
51
+ .nav-item { display:flex; align-items:center; gap:0.75rem; padding:0.875rem; border-radius:var(--radius); color:var(--text-muted); cursor:pointer; margin-bottom:0.25rem; font-weight:500; }
52
+ .nav-item.active { background:var(--primary); color:white; }
53
+ .nav-section { font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--text-muted); margin:1.5rem 0 0.5rem 0.75rem; font-weight:600; }
54
 
55
+ /*FOOTER SIDEBAR : profil + settings */
56
+ .sidebar-footer { margin-top:auto; padding-top:1rem; border-top:1px solid var(--border); display:flex; align-items:center; gap:0.75rem; }
57
+ .avatar-btn { width:40px; height:40px; border-radius:50%; overflow:hidden; background:var(--primary); border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
58
+ .avatar-btn img { width:100%; height:100%; object-fit:cover; }
59
+ .avatar-initial { color:white; font-weight:700; font-size:1rem; display:flex; align-items:center; justify-content:center; width:100%; height:100%; }
60
+ .avatar-initial.big { font-size:1.5rem; }
61
+ .footer-info { flex:1; min-width:0; display:flex; flex-direction:column; }
62
+ .footer-username { font-size:0.875rem; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
63
+ .footer-sub { font-size:0.7rem; color:var(--text-muted); }
64
+ .avatar-preview { width:72px; height:72px; border-radius:50%; overflow:hidden; background:var(--primary); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
65
+ .avatar-preview img { width:100%; height:100%; object-fit:cover; }
66
+ .settings-avatar-row { display:flex; gap:1rem; align-items:center; margin-bottom:1.25rem; }
67
+ .settings-section { margin-bottom:1.25rem; }
68
+ .owner-badge img { width:14px; height:14px; border-radius:50%; object-fit:cover; vertical-align:-2px; margin-right:4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ main { flex:1; display:flex; flex-direction:column; min-width:0; }
71
+ .toolbar { padding:0.75rem 1rem; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--border); background:var(--bg-panel); gap:0.5rem; flex-shrink:0; }
72
+ .breadcrumb { display:flex; align-items:center; gap:0.5rem; font-size:1rem; font-weight:600; flex:1; min-width:0; overflow:hidden; }
73
+ .breadcrumb span { color:var(--text-muted); cursor:pointer; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
74
+ .breadcrumb span:last-child { color:var(--text-main); }
75
+ .breadcrumb .sep { color:var(--border); flex-shrink:0; }
76
+ .actions { display:flex; gap:0.5rem; align-items:center; flex-shrink:0; }
77
+ .btn-icon { width:40px; height:40px; background:transparent; border:1px solid var(--border); border-radius:var(--radius); color:var(--text-muted); cursor:pointer; display:flex; align-items:center; justify-content:center; }
78
+ .btn-primary { background:var(--primary); color:white; border:none; padding:0 1rem; height:40px; border-radius:var(--radius); font-weight:500; cursor:pointer; display:flex; align-items:center; gap:0.5rem; }
79
+ .sort-select { height:40px; background:var(--bg-dark); border:1px solid var(--border); border-radius:var(--radius); color:var(--text-muted); font-size:0.8rem; padding:0 0.5rem; }
80
+ .file-area { flex:1; overflow-y:auto; padding:1rem; -webkit-overflow-scrolling:touch; }
81
+ .empty-state { text-align:center; padding:4rem 1rem; color:var(--text-muted); }
82
+ .empty-state .icon { margin-bottom:1rem; opacity:0.5; }
83
+ .section-title { margin:1.25rem 0 0.75rem; font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--text-muted); font-weight:600; }
84
+ .section-title:first-child { margin-top:0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:0.75rem; }
87
+ .card { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; cursor:pointer; display:flex; flex-direction:column; }
88
+ .card:active { transform:scale(0.97); border-color:var(--primary); }
89
+ .card-thumb { aspect-ratio:1/1; background:#000; display:flex; align-items:center; justify-content:center; overflow:hidden; position:relative; }
90
+ .card-thumb img { width:100%; height:100%; object-fit:cover; opacity:0.9; }
91
+ .card-thumb .icon-placeholder { width:36px; height:36px; color:var(--text-muted); opacity:0.5; }
92
+ .card-thumb.folder-thumb { background:var(--bg-hover); }
93
+ .card-thumb.folder-thumb .icon { width:42px; height:42px; color:var(--primary); }
94
+ .owner-badge { position:absolute; top:6px; left:6px; background:rgba(0,0,0,0.65); color:var(--primary); font-size:0.65rem; padding:2px 7px; border-radius:4px; font-weight:600; max-width:90%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
95
+ .card-info { padding:0.6rem 0.7rem; border-top:1px solid var(--border); }
96
+ .card-name { font-weight:500; font-size:0.85rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:0.2rem; }
97
+ .card-meta { display:flex; justify-content:space-between; gap:0.5rem; font-size:0.7rem; color:var(--text-muted); }
98
+ .card-meta span { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
99
 
100
+ .modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.85); backdrop-filter:blur(4px); display:none; align-items:center; justify-content:center; z-index:2000; padding:1rem; }
101
+ .modal { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); width:100%; max-width:500px; padding:1.5rem; max-height:90vh; overflow-y:auto; }
102
+ .modal-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem; }
103
+ .modal-title { font-size:1.25rem; font-weight:600; display:flex; align-items:center; gap:0.5rem; }
104
+ .close-modal { background:none; border:none; color:var(--text-muted); cursor:pointer; padding:0.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ #preview-modal .modal { max-width:100vw; max-height:100vh; background:var(--bg-dark); border:none; border-radius:0; padding:0; display:flex; flex-direction:column; width:100%; height:100%; }
107
+ #preview-container { flex:1; display:flex; align-items:center; justify-content:center; overflow:hidden; padding:1rem; }
108
+ #preview-content { max-width:100%; max-height:100%; object-fit:contain; }
109
+ .preview-controls { padding:1rem; border-top:1px solid var(--border); display:flex; gap:1rem; background:var(--bg-panel); }
110
+ .preview-controls .btn { flex:1; }
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ .tos-frame { width:100%; height:45vh; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-dark); margin-bottom:1.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ .dialog-message { font-size:0.9rem; color:var(--text-muted); margin-bottom:1rem; line-height:1.5; }
115
+ .dialog-actions { display:flex; gap:1rem; margin-top:1.5rem; }
116
+ .dialog-actions .btn { flex:1; }
 
 
 
 
 
 
117
 
118
+ .share-section { margin-bottom:1.25rem; }
119
+ .share-label { font-size:0.9rem; font-weight:600; display:flex; align-items:center; gap:0.4rem; margin-bottom:0.25rem; }
120
+ .share-hint { font-size:0.75rem; color:var(--text-muted); margin-bottom:0.5rem; }
121
+ .share-input-group { display:flex; gap:0.5rem; }
122
+ .share-input { flex:1; font-family:monospace; font-size:0.8rem; padding:0.6rem 0.75rem; background:var(--bg-dark); }
123
+ .share-copy-btn { width:44px; height:44px; background:var(--primary); color:white; border:none; flex-shrink:0; }
124
+ .share-copy-btn.copied { background:var(--success); }
125
+ .share-footer { font-size:0.75rem; color:var(--text-muted); text-align:center; padding-top:0.75rem; border-top:1px solid var(--border); }
 
126
 
127
+ .share-list { display:flex; flex-direction:column; gap:0.75rem; }
128
+ .share-item { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); padding:1rem; }
129
+ .share-item-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem; padding-bottom:0.75rem; border-bottom:1px solid var(--border); gap:0.5rem; }
130
+ .share-item-info { display:flex; align-items:center; gap:0.5rem; flex:1; min-width:0; }
131
+ .share-item-name { font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
132
+ .share-item-sub { font-size:0.75rem; color:var(--text-muted); }
133
+ .share-item-links { display:flex; flex-direction:column; gap:0.5rem; }
134
+ .share-link-row { display:flex; align-items:center; gap:0.5rem; }
135
+ .share-link-label { font-size:0.75rem; color:var(--text-muted); width:70px; flex-shrink:0; }
136
+ .share-link-input { flex:1; font-family:monospace; font-size:0.75rem; padding:0.5rem 0.75rem; background:var(--bg-dark); }
137
+ .share-item-footer { margin-top:0.75rem; padding-top:0.75rem; border-top:1px solid var(--border); font-size:0.75rem; color:var(--text-muted); text-align:center; }
 
 
138
 
139
+ #upload-overlay { position:fixed; bottom:0; left:0; right:0; background:var(--bg-panel); border-top:1px solid var(--border); padding:1rem; display:none; z-index:3000; padding-bottom:calc(1rem + env(safe-area-inset-bottom)); }
140
+ .progress-bar-bg { height:4px; background:var(--bg-dark); border-radius:2px; margin-top:0.75rem; overflow:hidden; }
141
+ .progress-bar-fill { height:100%; background:var(--primary); width:0%; transition:width 0.3s; }
142
+ .upload-status { font-size:0.875rem; color:var(--text-muted); margin-top:0.75rem; display:flex; justify-content:space-between; gap:1rem; }
143
+ .upload-status span:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
144
+ .upload-speed { color:var(--success); font-weight:600; }
145
 
146
+ #toast-container { position:fixed; top:1rem; right:1rem; left:1rem; z-index:4000; display:flex; flex-direction:column; gap:0.5rem; pointer-events:none; }
147
+ .toast { background:var(--bg-panel); border-left:4px solid var(--primary); padding:1rem; border-radius:4px; animation:slideIn 0.3s ease-out; pointer-events:auto; }
148
+ .toast.error { border-left-color:var(--danger); }
149
+ .toast.success { border-left-color:var(--success); }
150
+ @keyframes slideIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
 
 
 
 
 
 
151
 
152
+ #context-menu { position:fixed; background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); padding:0.5rem 0; min-width:220px; box-shadow:0 10px 25px rgba(0,0,0,0.5); display:none; z-index:5000; }
153
+ .ctx-item { padding:0.75rem 1rem; font-size:0.875rem; cursor:pointer; display:flex; align-items:center; gap:0.75rem; }
154
+ .ctx-sep { height:1px; background:var(--border); margin:0.25rem 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ .sidebar-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:90; }
157
+ .sidebar-overlay.active { display:block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ /* THÈME CLAIR */
160
+ body.light { --bg-dark:#f5f5f6; --bg-panel:#ffffff; --bg-hover:#ececee; --text-main:#17171b; --text-muted:#5b5b66; --border:#d8d8de; }
161
+ body.light .card-thumb { background:#e8e8ec; }
162
+ body.light .card-thumb.folder-thumb { background:#e0e0e6; }
163
+ body.light .form-input, body.light .share-input, body.light .share-link-input, body.light .sort-select { background:#f0f0f2; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ @media (max-width:768px) {
166
+ .menu-btn { display:flex; }
167
+ .user-badge { display:none; }
168
+ aside { position:fixed; left:0; top:60px; bottom:0; z-index:95; transform:translateX(-100%); }
169
+ aside.open { transform:translateX(0); }
170
+ .btn-primary .text { display:none; }
171
+ .btn-primary { padding:0; width:40px; justify-content:center; }
172
+ .sort-select { display:none; }
173
+ #context-menu { bottom:0 !important; top:auto !important; left:0 !important; right:0; width:100%; min-width:100%; border-radius:16px 16px 0 0; padding-bottom:env(safe-area-inset-bottom); }
174
+ .ctx-item { padding:1rem 1.5rem; font-size:1rem; }
 
 
175
  }
176
+ @media (max-width:480px) {
177
+ .grid { grid-template-columns:repeat(2,1fr); }
178
+ .file-area { padding:0.75rem; }
179
+ .auth-card { padding:1.5rem; }
180
+ .modal { padding:1.25rem; }
181
+ }
182
+ @media (min-width:769px) { .btn-primary .text { display:inline; } }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <svg style="display:none;">
187
+ <symbol id="icon-drive" viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></symbol>
188
+ <symbol id="icon-folder" viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></symbol>
189
+ <symbol id="icon-file" viewBox="0 0 24 24"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></symbol>
190
+ <symbol id="icon-upload" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></symbol>
191
+ <symbol id="icon-share" viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></symbol>
192
+ <symbol id="icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></symbol>
193
+ <symbol id="icon-trash" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></symbol>
194
+ <symbol id="icon-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></symbol>
195
+ <symbol id="icon-eye" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
196
+ <symbol id="icon-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></symbol>
197
+ <symbol id="icon-log-out" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></symbol>
198
+ <symbol id="icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></symbol>
199
+ <symbol id="icon-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
200
+ <symbol id="icon-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></symbol>
201
+ <symbol id="icon-user-plus" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></symbol>
202
+ <symbol id="icon-menu" viewBox="0 0 24 24"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></symbol>
203
+ <symbol id="icon-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></symbol>
204
+ </svg>
205
 
206
+ <div id="auth-screen">
207
+ <div class="auth-card">
208
+ <div class="auth-header"><h1><svg class="icon"><use href="#icon-drive"></use></svg> PixelDrive</h1></div>
209
+ <div id="login-form">
210
+ <div class="form-group"><label data-i18n="username">Utilisateur</label><input type="text" id="login-user" class="form-input" autocomplete="username"></div>
211
+ <div class="form-group"><label data-i18n="password">Mot de passe</label><input type="password" id="login-pass" class="form-input" autocomplete="current-password"></div>
212
+ <button class="btn" onclick="app.login()"><span data-i18n="login_btn">Se connecter</span></button>
213
+ <div class="auth-switch" id="switch-login"></div>
214
+ </div>
215
+ <div id="register-form" style="display:none;">
216
+ <div class="form-group"><label data-i18n="username">Utilisateur</label><input type="text" id="reg-user" class="form-input"></div>
217
+ <div class="form-group"><label data-i18n="password">Mot de passe</label><input type="password" id="reg-pass" class="form-input"></div>
218
+ <div class="form-group"><label data-i18n="confirm_pass">Confirmer</label><input type="password" id="reg-pass2" class="form-input"></div>
219
+ <div class="form-group" style="display:flex;align-items:center;gap:0.75rem;">
220
+ <input type="checkbox" id="reg-tos" style="width:20px;height:20px;accent-color:var(--primary);">
221
+ <label for="reg-tos" style="margin:0;font-size:0.875rem;"><span data-i18n="tos_accept">J'accepte les CGU</span> · <a href="#" onclick="app.showTos(false); return false;">CGU</a></label>
222
+ </div>
223
+ <button class="btn" onclick="app.register()"><span data-i18n="register_btn">Créer compte</span></button>
224
+ <div class="auth-switch" id="switch-register"></div>
225
+ </div>
226
+ </div>
227
+ </div>
228
 
229
+ <div id="app-screen">
230
+ <header>
231
+ <div class="header-left">
232
+ <button class="menu-btn" onclick="app.toggleSidebar()"><svg class="icon"><use href="#icon-menu"></use></svg></button>
233
+ <div class="logo"><svg class="icon"><use href="#icon-drive"></use></svg> PixelDrive</div>
234
+ </div>
235
+ <div class="user-menu">
236
+ <span class="user-badge" id="user-display">User</span>
237
+ <button class="btn-icon" onclick="app.logout()"><svg class="icon"><use href="#icon-log-out"></use></svg></button>
238
+ </div>
239
+ </header>
240
+ <div class="sidebar-overlay" onclick="app.toggleSidebar()"></div>
241
+ <div class="main-container">
242
+ <aside id="sidebar">
243
+ <div class="nav-section" data-i18n="nav_section">Navigation</div>
244
+ <div class="nav-item active" onclick="app.navTo('files')" id="nav-files"><svg class="icon"><use href="#icon-folder"></use></svg> <span data-i18n="nav_files">Mes Fichiers</span></div>
245
+ <div class="nav-item" onclick="app.navTo('shared')" id="nav-shared"><svg class="icon"><use href="#icon-share"></use></svg> <span data-i18n="nav_shared">Partagés</span></div>
246
+ <div class="nav-item" onclick="app.navTo('recent')" id="nav-recent"><svg class="icon"><use href="#icon-clock"></use></svg> <span data-i18n="nav_recent">Récents</span></div>
247
+ <!-- ✅ FOOTER : avatar + profil + settings -->
248
+ <div class="sidebar-footer">
249
+ <button class="avatar-btn" onclick="app.openSettings()" aria-label="Profil">
250
+ <img id="avatar-img" alt="" style="display:none;">
251
+ <span id="avatar-initial" class="avatar-initial">?</span>
252
+ </button>
253
+ <div class="footer-info">
254
+ <span class="footer-username" id="footer-username">User</span>
255
+ <span class="footer-sub" data-i18n="settings">Paramètres</span>
256
+ </div>
257
+ <button class="btn-icon" onclick="app.openSettings()" title="Paramètres" data-i18n-title="settings">
258
+ <svg class="icon"><use href="#icon-settings"></use></svg>
259
+ </button>
260
+ </div>
261
+ </aside>
262
+ <main>
263
+ <div class="toolbar">
264
+ <div class="breadcrumb" id="breadcrumb"><span onclick="app.goRoot()">Mes Fichiers</span></div>
265
+ <div class="actions" id="toolbar-actions">
266
+ <button class="btn-primary" onclick="document.getElementById('file-input').click()"><svg class="icon icon-sm"><use href="#icon-upload"></use></svg> <span class="text" data-i18n="upload">Uploader</span></button>
267
+ <input type="file" id="file-input" style="display:none;" multiple onchange="app.handleUpload(this)">
268
+ <button class="btn-icon" onclick="app.createFolder()" title="Nouveau Dossier" data-i18n-title="new_folder"><svg class="icon"><use href="#icon-plus"></use></svg></button>
269
+ <select id="sort-select" class="sort-select" onchange="app.setSort(this.value)">
270
+ <option value="name-asc" data-i18n="sort_name_asc">Nom (A-Z)</option>
271
+ <option value="name-desc" data-i18n="sort_name_desc">Nom (Z-A)</option>
272
+ <option value="size-desc" data-i18n="sort_size">Taille</option>
273
+ <option value="date-desc" data-i18n="sort_date">Date</option>
274
+ </select>
275
+ <button class="btn-icon" onclick="app.refresh()" title="Rafraîchir" data-i18n-title="refresh"><svg class="icon"><use href="#icon-refresh"></use></svg></button>
276
+ </div>
277
+ </div>
278
+ <div class="file-area" id="file-area"></div>
279
+ </main>
280
+ </div>
281
+ </div>
282
 
283
+ <div id="upload-overlay">
284
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;">
285
+ <strong><span data-i18n="transfer">Transfert</span> <span id="upload-count"></span></strong>
286
+ <button class="btn-icon" style="width:32px;height:32px;" onclick="app.cancelUpload()"><svg class="icon icon-sm"><use href="#icon-x"></use></svg></button>
287
+ </div>
288
+ <div class="progress-bar-bg"><div class="progress-bar-fill" id="upload-progress"></div></div>
289
+ <div class="upload-status">
290
+ <span id="upload-filename"></span>
291
+ <span><span id="upload-speed" class="upload-speed"></span> <span id="upload-percent" style="font-weight:600;">0%</span></span>
292
+ </div>
293
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
+ <div class="modal-overlay" id="preview-modal" onclick="if(event.target === this) app.closePreview()">
296
+ <div class="modal">
297
+ <div id="preview-container"></div>
298
+ <div class="preview-controls">
299
+ <button class="btn btn-secondary" onclick="app.closePreview()"><span data-i18n="close">Fermer</span></button>
300
+ <button class="btn btn-primary" id="preview-download-btn"><svg class="icon icon-sm"><use href="#icon-download"></use></svg> <span data-i18n="download">Télécharger</span></button>
301
+ </div>
302
+ </div>
303
+ </div>
304
 
305
+ <div class="modal-overlay" id="share-modal" onclick="if(event.target === this) app.closeShare()">
306
+ <div class="modal">
307
+ <div class="modal-header">
308
+ <div class="modal-title"><svg class="icon"><use href="#icon-share"></use></svg> <span data-i18n="share_title">Partager le fichier</span></div>
309
+ <button class="close-modal" onclick="app.closeShare()"><svg class="icon"><use href="#icon-x"></use></svg></button>
310
+ </div>
311
+ <div class="share-section">
312
+ <label class="share-label"><svg class="icon icon-sm"><use href="#icon-eye"></use></svg> <span data-i18n="share_preview">Lien de prévisualisation</span></label>
313
+ <p class="share-hint" data-i18n="share_preview_hint">Page web avec lecteur intégré</p>
314
+ <div class="share-input-group">
315
+ <input type="text" id="share-preview-url" readonly class="form-input share-input" value="Génération...">
316
+ <button class="btn-icon share-copy-btn" data-type="preview"><svg class="icon"><use href="#icon-link"></use></svg></button>
317
+ </div>
318
+ </div>
319
+ <div class="share-section">
320
+ <label class="share-label"><svg class="icon icon-sm"><use href="#icon-download"></use></svg> <span data-i18n="share_dl">Lien de téléchargement direct</span></label>
321
+ <p class="share-hint" data-i18n="share_dl_hint">Télécharge le fichier immédiatement</p>
322
+ <div class="share-input-group">
323
+ <input type="text" id="share-download-url" readonly class="form-input share-input" value="Génération...">
324
+ <button class="btn-icon share-copy-btn" data-type="download"><svg class="icon"><use href="#icon-link"></use></svg></button>
325
+ </div>
326
+ </div>
327
+ <div class="share-section">
328
+ <label class="share-label"><svg class="icon icon-sm"><use href="#icon-link"></use></svg> <span data-i18n="share_embed">Code d'intégration HTML</span></label>
329
+ <p class="share-hint" data-i18n="share_embed_hint">À coller sur un site web ou un forum</p>
330
+ <div class="share-input-group">
331
+ <input type="text" id="share-embed-code" readonly class="form-input share-input" value="Génération...">
332
+ <button class="btn-icon share-copy-btn" data-type="embed"><svg class="icon"><use href="#icon-link"></use></svg></button>
333
+ </div>
334
+ </div>
335
+ <div class="share-footer" data-i18n="share_expiry">Ces liens expirent dans 7 jours</div>
336
+ </div>
337
+ </div>
338
 
339
+ <div class="modal-overlay" id="tos-modal">
340
+ <div class="modal">
341
+ <div class="modal-header">
342
+ <div class="modal-title" data-i18n="tos_title">Conditions Générales d'Utilisation</div>
343
+ <button class="close-modal" onclick="app.hideTos()"><svg class="icon"><use href="#icon-x"></use></svg></button>
344
+ </div>
345
+ <iframe id="tos-frame" src="/tos.html" class="tos-frame" title="CGU PixelDrive"></iframe>
346
+ <div style="display:flex;gap:1rem;">
347
+ <button class="btn btn-secondary" style="flex:1;" onclick="app.hideTos()"><span data-i18n="cancel">Annuler</span></button>
348
+ <button class="btn btn-primary" id="tos-accept-btn" style="display:none;flex:1;" onclick="app.acceptTos()"><span data-i18n="tos_accept_btn">J'accepte</span></button>
349
+ </div>
350
+ </div>
351
+ </div>
 
 
 
 
 
352
 
353
+ <div class="modal-overlay" id="dialog-modal">
354
+ <div class="modal" style="max-width:420px;">
355
+ <div class="modal-header"><div class="modal-title" id="dialog-title"></div></div>
356
+ <div class="dialog-message" id="dialog-message"></div>
357
+ <div id="dialog-input-wrap" style="display:none;">
358
+ <input type="text" id="dialog-input" class="form-input">
359
+ </div>
360
+ <div class="dialog-actions">
361
+ <button class="btn btn-secondary" id="dialog-cancel" onclick="app._dialogClose(false)"><span data-i18n="cancel">Annuler</span></button>
362
+ <button class="btn" id="dialog-ok" onclick="app._dialogClose(true)">Confirmer</button>
363
+ </div>
364
+ </div>
365
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ <!-- MODALE SETTINGS -->
368
+ <div class="modal-overlay" id="settings-modal" onclick="if(event.target === this) app.closeSettings()">
369
+ <div class="modal" style="max-width:460px;">
370
+ <div class="modal-header">
371
+ <div class="modal-title"><svg class="icon"><use href="#icon-settings"></use></svg> <span data-i18n="settings">Paramètres</span></div>
372
+ <button class="close-modal" onclick="app.closeSettings()"><svg class="icon"><use href="#icon-x"></use></svg></button>
373
+ </div>
374
+ <div class="settings-section">
375
+ <label class="share-label" data-i18n="profile">Profil</label>
376
+ <div class="settings-avatar-row">
377
+ <div class="avatar-preview"><img id="settings-avatar-img" alt="" style="display:none;"><span id="settings-avatar-initial" class="avatar-initial big">?</span></div>
378
+ <div style="flex:1;display:flex;flex-direction:column;gap:0.5rem;">
379
+ <button class="btn" style="padding:0.6rem;" onclick="document.getElementById('avatar-input').click()"><span data-i18n="change_avatar">Changer l'avatar</span></button>
380
+ <input type="file" id="avatar-input" accept="image/*" style="display:none;" onchange="app.uploadAvatar(this)">
381
+ <button class="btn btn-secondary" style="padding:0.6rem;" onclick="app.removeAvatar()"><span data-i18n="remove_avatar">Retirer</span></button>
382
+ </div>
383
+ </div>
384
+ </div>
385
+ <div class="settings-section">
386
+ <label class="share-label" data-i18n="language">Langue</label>
387
+ <select id="set-lang" class="form-input" onchange="app.setLang(this.value)"><option value="fr">Français</option><option value="en">English</option></select>
388
+ </div>
389
+ <div class="settings-section">
390
+ <label class="share-label" data-i18n="theme">Thème</label>
391
+ <select id="set-theme" class="form-input" onchange="app.setTheme(this.value)"><option value="dark" data-i18n="theme_dark">Sombre</option><option value="light" data-i18n="theme_light">Clair</option></select>
392
+ </div>
393
+ <div class="settings-section">
394
+ <label class="share-label" data-i18n="default_sort">Tri par défaut</label>
395
+ <select id="set-sort" class="form-input" onchange="app.setDefSort(this.value)">
396
+ <option value="name-asc" data-i18n="sort_name_asc">Nom (A-Z)</option>
397
+ <option value="name-desc" data-i18n="sort_name_desc">Nom (Z-A)</option>
398
+ <option value="size-desc" data-i18n="sort_size">Taille</option>
399
+ <option value="date-desc" data-i18n="sort_date">Date</option>
400
+ </select>
401
+ </div>
402
+ </div>
403
+ </div>
404
 
405
+ <div id="context-menu">
406
+ <div class="ctx-item" id="ctx-preview"><svg class="icon icon-sm"><use href="#icon-eye"></use></svg> <span data-i18n="ctx_preview">Aperçu</span></div>
407
+ <div class="ctx-item" id="ctx-download"><svg class="icon icon-sm"><use href="#icon-download"></use></svg> <span data-i18n="ctx_download">Télécharger</span></div>
408
+ <div class="ctx-item" id="ctx-share"><svg class="icon icon-sm"><use href="#icon-link"></use></svg> <span data-i18n="ctx_link">Lien public</span></div>
409
+ <div class="ctx-item" id="ctx-share-user"><svg class="icon icon-sm"><use href="#icon-user-plus"></use></svg> <span data-i18n="ctx_share_user">Partager à un utilisateur</span></div>
410
+ <div class="ctx-sep"></div>
411
+ <div class="ctx-item" id="ctx-remove" style="color:var(--danger);"><svg class="icon icon-sm"><use href="#icon-x"></use></svg> <span data-i18n="ctx_remove">Retirer de ma liste</span></div>
412
+ <div class="ctx-item" id="ctx-delete" style="color:var(--danger);"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg> <span data-i18n="ctx_delete">Supprimer</span></div>
413
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ <div id="toast-container"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
+ <script src="/i18n.js"></script>
418
+ <script src="/app.js"></script>
419
+ </body>
420
+ </html>