Spaces:
Paused
Paused
Update public/app.js
Browse files- public/app.js +391 -481
public/app.js
CHANGED
|
@@ -1,510 +1,420 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 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 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 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 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 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 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 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 |
-
|
| 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 |
-
|
| 186 |
-
|
| 187 |
-
|
| 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 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
});
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
area.appendChild(grid);
|
| 217 |
-
},
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 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 |
-
|
| 239 |
-
|
| 240 |
-
|
| 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 |
-
|
| 266 |
-
|
| 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 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 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 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
if (result) okCount++; else errCount++;
|
| 346 |
-
if (this._uploadCancelled) { this._uploadCancelled = false; break; }
|
| 347 |
}
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 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 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 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 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|