Spaces:
Paused
Paused
Update public/app.js
Browse files- public/app.js +552 -392
public/app.js
CHANGED
|
@@ -1,420 +1,580 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 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 |
-
|
| 30 |
-
|
| 31 |
-
.
|
| 32 |
-
.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.
|
| 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 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
.
|
| 52 |
-
.
|
| 53 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
.
|
| 63 |
-
.
|
| 64 |
-
.
|
| 65 |
-
|
| 66 |
-
|
| 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 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
.
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 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 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
.
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
.
|
| 122 |
-
.
|
| 123 |
-
.
|
| 124 |
-
.
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
.
|
| 131 |
-
|
| 132 |
-
.
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
.
|
| 142 |
-
.
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
.
|
| 149 |
-
.
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
.
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 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 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 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 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
<
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
-
|
| 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 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 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 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 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 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
-
|
| 418 |
-
<script src="/app.js"></script>
|
| 419 |
-
</body>
|
| 420 |
-
</html>
|
|
|
|
| 1 |
+
// ==========================================
|
| 2 |
+
// PIXELDRIVE v4.1 - APP.JS (i18n + avatars + settings)
|
| 3 |
+
// ==========================================
|
| 4 |
+
const app = {
|
| 5 |
+
user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
|
| 6 |
+
files: [], folders: [], uploadController: null, _uploadCancelled: false,
|
| 7 |
+
isMobile: window.innerWidth <= 768, suppressClick: false,
|
| 8 |
+
_dialogResolver: null, _dialogOpts: null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
async init() {
|
| 11 |
+
// ✅ Préférences locales (thème, tri) + i18n
|
| 12 |
+
document.body.classList.toggle('light', (localStorage.getItem('pd_theme') || 'dark') === 'light');
|
| 13 |
+
this.sortMode = localStorage.getItem('pd_sort') || 'name-asc';
|
| 14 |
+
const ss = document.getElementById('sort-select'); if (ss) ss.value = this.sortMode;
|
| 15 |
+
applyI18n();
|
| 16 |
+
this.applyDynamicI18n();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
+
try {
|
| 19 |
+
const res = await fetch('/api/me');
|
| 20 |
+
if (res.ok) {
|
| 21 |
+
this.user = await res.json();
|
| 22 |
+
this.showApp();
|
| 23 |
+
if (!this.user.tosAccepted) this.showTos(true);
|
| 24 |
+
this.loadContent();
|
| 25 |
+
}
|
| 26 |
+
} catch (e) { console.error(e); }
|
| 27 |
+
document.addEventListener('click', (e) => { if (!document.getElementById('context-menu').contains(e.target)) this.closeMenu(); });
|
| 28 |
+
document.getElementById('file-area').addEventListener('contextmenu', (e) => e.preventDefault());
|
| 29 |
+
window.addEventListener('resize', () => {
|
| 30 |
+
this.isMobile = window.innerWidth <= 768;
|
| 31 |
+
if (!this.isMobile) { document.getElementById('sidebar').classList.remove('open'); document.querySelector('.sidebar-overlay').classList.remove('active'); }
|
| 32 |
+
});
|
| 33 |
+
document.getElementById('login-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.login(); });
|
| 34 |
+
document.getElementById('reg-pass2').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.register(); });
|
| 35 |
+
},
|
| 36 |
|
| 37 |
+
// ---------- UTILS ----------
|
| 38 |
+
esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); },
|
| 39 |
+
formatSize(b) { if (b === null || b === undefined) return '—'; if (b < 1024) return b + ' B'; if (b < 1048576) return (b/1024).toFixed(0) + ' KB'; if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB'; return (b/1073741824).toFixed(2) + ' GB'; },
|
| 40 |
+
statusLabel(s) { return ({pending:t('st_pending'), uploading:t('st_uploading'), processing:t('st_processing'), ready:t('st_ready'), error:t('st_error')})[s] || s; },
|
| 41 |
+
formatDate(ts) { return ts ? new Date(ts).toLocaleString('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; },
|
| 42 |
+
toast(msg, type) {
|
| 43 |
+
const el = document.createElement('div');
|
| 44 |
+
el.className = 'toast ' + (type || 'info');
|
| 45 |
+
el.textContent = msg;
|
| 46 |
+
document.getElementById('toast-container').appendChild(el);
|
| 47 |
+
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
|
| 48 |
+
},
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
// ✅ Lignes composites (texte + lien) traduites
|
| 51 |
+
_switchHTML(key, onclick) {
|
| 52 |
+
const s = t(key);
|
| 53 |
+
const i = s.indexOf('?');
|
| 54 |
+
if (i < 0) return this.esc(s);
|
| 55 |
+
return this.esc(s.slice(0, i + 1)) + ' <a onclick="' + onclick + '">' + this.esc(s.slice(i + 1).trim()) + '</a>';
|
| 56 |
+
},
|
| 57 |
+
applyDynamicI18n() {
|
| 58 |
+
const sw1 = document.getElementById('switch-login');
|
| 59 |
+
if (sw1) sw1.innerHTML = this._switchHTML('no_account', "app.toggleAuth('register')");
|
| 60 |
+
const sw2 = document.getElementById('switch-register');
|
| 61 |
+
if (sw2) sw2.innerHTML = this._switchHTML('have_account', "app.toggleAuth('login')");
|
| 62 |
+
},
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
// ✅ DIALOGUES CUSTOM
|
| 65 |
+
dialog(opts) {
|
| 66 |
+
return new Promise(resolve => {
|
| 67 |
+
this._dialogResolver = resolve;
|
| 68 |
+
this._dialogOpts = opts;
|
| 69 |
+
document.getElementById('dialog-title').textContent = opts.title || '';
|
| 70 |
+
const msg = document.getElementById('dialog-message');
|
| 71 |
+
msg.textContent = opts.message || '';
|
| 72 |
+
msg.style.display = opts.message ? 'block' : 'none';
|
| 73 |
+
const wrap = document.getElementById('dialog-input-wrap');
|
| 74 |
+
const input = document.getElementById('dialog-input');
|
| 75 |
+
if (opts.type === 'prompt') {
|
| 76 |
+
wrap.style.display = 'block';
|
| 77 |
+
input.value = opts.value || '';
|
| 78 |
+
input.placeholder = opts.placeholder || '';
|
| 79 |
+
setTimeout(() => input.focus(), 60);
|
| 80 |
+
} else wrap.style.display = 'none';
|
| 81 |
+
input.onkeydown = (e) => { if (e.key === 'Enter') this._dialogClose(true); };
|
| 82 |
+
const ok = document.getElementById('dialog-ok');
|
| 83 |
+
ok.textContent = opts.confirmText || 'Confirmer';
|
| 84 |
+
ok.style.background = opts.danger ? 'var(--danger)' : 'var(--primary)';
|
| 85 |
+
document.getElementById('dialog-modal').style.display = 'flex';
|
| 86 |
+
});
|
| 87 |
+
},
|
| 88 |
+
_dialogClose(okClicked) {
|
| 89 |
+
document.getElementById('dialog-modal').style.display = 'none';
|
| 90 |
+
const opts = this._dialogOpts || {};
|
| 91 |
+
const value = document.getElementById('dialog-input').value.trim();
|
| 92 |
+
const r = this._dialogResolver;
|
| 93 |
+
this._dialogResolver = null;
|
| 94 |
+
if (r) r(okClicked ? (opts.type === 'prompt' ? (value || null) : true) : null);
|
| 95 |
+
},
|
| 96 |
+
confirm(opts) { return this.dialog(Object.assign({ type: 'confirm' }, opts)); },
|
| 97 |
+
prompt(opts) { return this.dialog(Object.assign({ type: 'prompt' }, opts)); },
|
| 98 |
|
| 99 |
+
// ---------- AUTH ----------
|
| 100 |
+
toggleAuth(m) { document.getElementById('login-form').style.display = m === 'login' ? 'block' : 'none'; document.getElementById('register-form').style.display = m === 'register' ? 'block' : 'none'; },
|
| 101 |
+
async login() {
|
| 102 |
+
const u = document.getElementById('login-user').value.trim(), p = document.getElementById('login-pass').value;
|
| 103 |
+
if (!u || !p) return this.toast('Champs requis', 'error');
|
| 104 |
+
try {
|
| 105 |
+
const res = await fetch('/api/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({u, p})});
|
| 106 |
+
const d = await res.json();
|
| 107 |
+
if (d.ok) { this.user = d.user; this.showApp(); if (!this.user.tosAccepted) this.showTos(true); this.navTo('files'); }
|
| 108 |
+
else this.toast(d.error || 'Erreur', 'error');
|
| 109 |
+
} catch (e) { this.toast('Erreur réseau', 'error'); }
|
| 110 |
+
},
|
| 111 |
+
async register() {
|
| 112 |
+
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;
|
| 113 |
+
if (!u || !p) return this.toast('Champs requis', 'error');
|
| 114 |
+
if (p !== p2) return this.toast('Mots de passe différents', 'error');
|
| 115 |
+
if (!tos) return this.toast('Acceptez les CGU', 'error');
|
| 116 |
+
try {
|
| 117 |
+
const res = await fetch('/api/register', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({u, p, t:true})});
|
| 118 |
+
const d = await res.json();
|
| 119 |
+
if (d.ok) { this.toast('Compte créé !', 'success'); this.toggleAuth('login'); } else this.toast(d.error, 'error');
|
| 120 |
+
} catch (e) { this.toast('Erreur', 'error'); }
|
| 121 |
+
},
|
| 122 |
+
async logout() {
|
| 123 |
+
const ok = await this.confirm({ title: t('dlg_logout_title'), message: t('dlg_logout_msg'), confirmText: t('dlg_logout_title'), danger: true });
|
| 124 |
+
if (!ok) return;
|
| 125 |
+
await fetch('/api/logout', {method:'POST'});
|
| 126 |
+
location.reload();
|
| 127 |
+
},
|
| 128 |
+
showApp() {
|
| 129 |
+
document.getElementById('auth-screen').style.display = 'none';
|
| 130 |
+
document.getElementById('app-screen').style.display = 'flex';
|
| 131 |
+
document.getElementById('user-display').textContent = this.user.username;
|
| 132 |
+
document.getElementById('footer-username').textContent = this.user.username;
|
| 133 |
+
this.loadAvatar();
|
| 134 |
+
},
|
| 135 |
|
| 136 |
+
// ---------- CGU ----------
|
| 137 |
+
showTos(force) {
|
| 138 |
+
document.getElementById('tos-modal').style.display = 'flex';
|
| 139 |
+
document.getElementById('tos-accept-btn').style.display = force ? 'flex' : 'none';
|
| 140 |
+
},
|
| 141 |
+
hideTos() { document.getElementById('tos-modal').style.display = 'none'; },
|
| 142 |
+
async acceptTos() { await fetch('/api/tos/accept', {method:'POST'}); this.user.tosAccepted = true; this.hideTos(); this.toast('CGU ✓', 'success'); },
|
| 143 |
|
| 144 |
+
// ✅ AVATARS
|
| 145 |
+
loadAvatar() {
|
| 146 |
+
if (!this.user) return;
|
| 147 |
+
const url = '/api/users/' + this.user.id + '/avatar?' + Date.now();
|
| 148 |
+
const letter = (this.user.username || '?')[0].toUpperCase();
|
| 149 |
+
const imgs = [document.getElementById('avatar-img'), document.getElementById('settings-avatar-img')];
|
| 150 |
+
const inits = [document.getElementById('avatar-initial'), document.getElementById('settings-avatar-initial')];
|
| 151 |
+
imgs.forEach((img, i) => {
|
| 152 |
+
if (!img) return;
|
| 153 |
+
img.onerror = () => { img.style.display = 'none'; if (inits[i]) { inits[i].style.display = 'flex'; inits[i].textContent = letter; } };
|
| 154 |
+
img.onload = () => { img.style.display = 'block'; if (inits[i]) inits[i].style.display = 'none'; };
|
| 155 |
+
img.src = url;
|
| 156 |
+
});
|
| 157 |
+
},
|
| 158 |
+
async uploadAvatar(input) {
|
| 159 |
+
const f = input.files[0]; input.value = '';
|
| 160 |
+
if (!f) return;
|
| 161 |
+
const reader = new FileReader();
|
| 162 |
+
reader.onload = async () => {
|
| 163 |
+
const res = await fetch('/api/avatar', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ data: reader.result }) });
|
| 164 |
+
const d = await res.json();
|
| 165 |
+
if (d.ok) { this.toast(t('t_avatar_saved'), 'success'); this.loadAvatar(); }
|
| 166 |
+
else this.toast(d.error || 'Erreur', 'error');
|
| 167 |
+
};
|
| 168 |
+
reader.readAsDataURL(f);
|
| 169 |
+
},
|
| 170 |
+
async removeAvatar() {
|
| 171 |
+
await fetch('/api/avatar', { method:'DELETE' });
|
| 172 |
+
this.toast(t('t_avatar_removed'), 'success');
|
| 173 |
+
this.loadAvatar();
|
| 174 |
+
},
|
| 175 |
|
| 176 |
+
// ✅ SETTINGS
|
| 177 |
+
openSettings() {
|
| 178 |
+
document.getElementById('settings-modal').style.display = 'flex';
|
| 179 |
+
document.getElementById('set-lang').value = getLang();
|
| 180 |
+
document.getElementById('set-theme').value = localStorage.getItem('pd_theme') || 'dark';
|
| 181 |
+
document.getElementById('set-sort').value = this.sortMode;
|
| 182 |
+
this.loadAvatar();
|
| 183 |
+
},
|
| 184 |
+
closeSettings() { document.getElementById('settings-modal').style.display = 'none'; },
|
| 185 |
+
setLang(l) {
|
| 186 |
+
localStorage.setItem('pd_lang', l);
|
| 187 |
+
applyI18n();
|
| 188 |
+
this.applyDynamicI18n();
|
| 189 |
+
this.toast(t('t_lang_changed'), 'success');
|
| 190 |
+
this.refresh();
|
| 191 |
+
},
|
| 192 |
+
setTheme(th) { localStorage.setItem('pd_theme', th); document.body.classList.toggle('light', th === 'light'); },
|
| 193 |
+
setDefSort(m) { localStorage.setItem('pd_sort', m); this.sortMode = m; const s = document.getElementById('sort-select'); if (s) s.value = m; this.refresh(); },
|
| 194 |
|
| 195 |
+
// ---------- NAVIGATION ----------
|
| 196 |
+
toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.querySelector('.sidebar-overlay').classList.toggle('active'); },
|
| 197 |
+
navTo(section) {
|
| 198 |
+
this.currentSection = section;
|
| 199 |
+
this.currentFolder = null;
|
| 200 |
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
| 201 |
+
document.getElementById('nav-' + section).classList.add('active');
|
| 202 |
+
if (this.isMobile) this.toggleSidebar();
|
| 203 |
+
document.getElementById('toolbar-actions').style.display = (section === 'files') ? 'flex' : 'none';
|
| 204 |
+
this.refresh();
|
| 205 |
+
},
|
| 206 |
+
goRoot() {
|
| 207 |
+
if (this.currentSection === 'shared') { this.currentFolder = null; this.loadSharedPage(); return; }
|
| 208 |
+
this.currentFolder = null;
|
| 209 |
+
this.loadContent();
|
| 210 |
+
},
|
| 211 |
+
refresh() {
|
| 212 |
+
if (this.currentSection === 'files') this.loadContent();
|
| 213 |
+
else if (this.currentSection === 'shared') this.currentFolder ? this.loadContent() : this.loadSharedPage();
|
| 214 |
+
else this.loadRecent();
|
| 215 |
+
},
|
| 216 |
+
setSort(m) { this.sortMode = m; this.refresh(); },
|
| 217 |
+
sortFiles(files) {
|
| 218 |
+
const a = [...files];
|
| 219 |
+
if (this.sortMode === 'name-asc') a.sort((x,y) => x.name.localeCompare(y.name, undefined, {sensitivity:'base'}));
|
| 220 |
+
else if (this.sortMode === 'name-desc') a.sort((x,y) => y.name.localeCompare(x.name, undefined, {sensitivity:'base'}));
|
| 221 |
+
else if (this.sortMode === 'size-desc') a.sort((x,y) => (y.size||0) - (x.size||0));
|
| 222 |
+
else a.sort((x,y) => (y.updated_at||y.created_at||0) - (x.updated_at||x.created_at||0));
|
| 223 |
+
return a;
|
| 224 |
+
},
|
| 225 |
|
| 226 |
+
// ---------- FILES ----------
|
| 227 |
+
async loadContent() {
|
| 228 |
+
const area = document.getElementById('file-area');
|
| 229 |
+
area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
|
| 230 |
+
const bc = document.getElementById('breadcrumb');
|
| 231 |
+
bc.innerHTML = (this.currentSection === 'shared' ? '<span onclick="app.goRoot()">' + t('nav_shared') + '</span>' : '<span onclick="app.goRoot()">' + t('nav_files') + '</span>') +
|
| 232 |
+
(this.currentFolder ? ' <span class="sep">/</span> <span>' + this.esc(this.currentFolder.name) + '</span>' : '');
|
| 233 |
+
try {
|
| 234 |
+
const [fRes, dRes] = await Promise.all([
|
| 235 |
+
fetch('/api/files?folder_id=' + (this.currentFolder ? this.currentFolder.id : '')),
|
| 236 |
+
fetch('/api/folders?parent_id=' + (this.currentFolder ? this.currentFolder.id : ''))
|
| 237 |
+
]);
|
| 238 |
+
this.files = await fRes.json();
|
| 239 |
+
this.folders = await dRes.json();
|
| 240 |
+
this.renderGrid(this.folders, this.files, this.currentSection === 'shared' ? 'shared' : 'mine');
|
| 241 |
+
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 242 |
+
},
|
| 243 |
|
| 244 |
+
async loadRecent() {
|
| 245 |
+
const area = document.getElementById('file-area');
|
| 246 |
+
area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
|
| 247 |
+
document.getElementById('breadcrumb').innerHTML = '<span>' + t('nav_recent') + '</span>';
|
| 248 |
+
try {
|
| 249 |
+
const all = [];
|
| 250 |
+
const walk = async (pid) => {
|
| 251 |
+
const fr = await fetch('/api/files?folder_id=' + (pid || ''));
|
| 252 |
+
(await fr.json()).forEach(f => all.push(f));
|
| 253 |
+
const dr = await fetch('/api/folders?parent_id=' + (pid || ''));
|
| 254 |
+
for (const d of await dr.json()) await walk(d.id);
|
| 255 |
+
};
|
| 256 |
+
await walk(null);
|
| 257 |
+
this.renderGrid([], this.sortFiles(all).slice(0, 20), 'mine');
|
| 258 |
+
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 259 |
+
},
|
| 260 |
|
| 261 |
+
renderGrid(folders, files, mode) {
|
| 262 |
+
const area = document.getElementById('file-area');
|
| 263 |
+
area.innerHTML = '';
|
| 264 |
+
if (!folders.length && !files.length) {
|
| 265 |
+
area.innerHTML = '<div class="empty-state"><svg class="icon icon-lg"><use href="#icon-folder"></use></svg><p>' + t('empty_folder') + '</p></div>';
|
| 266 |
+
return;
|
| 267 |
+
}
|
| 268 |
+
const grid = document.createElement('div');
|
| 269 |
+
grid.className = 'grid';
|
| 270 |
|
| 271 |
+
folders.forEach(f => {
|
| 272 |
+
const el = document.createElement('div');
|
| 273 |
+
el.className = 'card';
|
| 274 |
+
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>' + t('folder') + '</span><span></span></div></div>';
|
| 275 |
+
el.onclick = () => { if (this.suppressClick) { this.suppressClick = false; return; } this.currentFolder = f; this.loadContent(); };
|
| 276 |
+
el.oncontextmenu = (e) => this.showContextMenu(e, mode === 'shared' ? 'shared-folder' : 'folder', f);
|
| 277 |
+
this.bindLongPress(el, mode === 'shared' ? 'shared-folder' : 'folder', f);
|
| 278 |
+
grid.appendChild(el);
|
| 279 |
+
});
|
| 280 |
|
| 281 |
+
this.sortFiles(files).forEach(f => {
|
| 282 |
+
const el = document.createElement('div');
|
| 283 |
+
el.className = 'card';
|
| 284 |
+
let thumb = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
|
| 285 |
+
if (f.mime && (f.mime.startsWith('image/') || f.mime.startsWith('video/'))) thumb = '<img src="/api/files/' + f.id + '/thumb" alt="" loading="lazy">';
|
| 286 |
+
el.innerHTML = '<div class="card-thumb">' + (f.owner_name ? '<span class="owner-badge"><img src="/api/users/' + f.owner_id + '/avatar" onerror="this.remove()">' + 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>';
|
| 287 |
+
el.onclick = () => { if (this.suppressClick) { this.suppressClick = false; return; } if (f.status === 'ready') this.previewFile(f); };
|
| 288 |
+
el.oncontextmenu = (e) => this.showContextMenu(e, mode === 'shared' ? 'shared-file' : 'file', f);
|
| 289 |
+
this.bindLongPress(el, mode === 'shared' ? 'shared-file' : 'file', f);
|
| 290 |
+
grid.appendChild(el);
|
| 291 |
+
});
|
| 292 |
+
area.appendChild(grid);
|
| 293 |
+
},
|
| 294 |
|
| 295 |
+
bindLongPress(el, type, item) {
|
| 296 |
+
let t2 = null;
|
| 297 |
+
el.addEventListener('touchstart', (e) => { t2 = setTimeout(() => { this.suppressClick = true; this.showContextMenu(e, type, item); }, 500); }, {passive:true});
|
| 298 |
+
el.addEventListener('touchend', () => clearTimeout(t2));
|
| 299 |
+
el.addEventListener('touchmove', () => clearTimeout(t2));
|
| 300 |
+
},
|
| 301 |
|
| 302 |
+
// ✅ ONGLET PARTAGÉS (3 sections)
|
| 303 |
+
async loadSharedPage() {
|
| 304 |
+
const area = document.getElementById('file-area');
|
| 305 |
+
area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
|
| 306 |
+
document.getElementById('breadcrumb').innerHTML = '<span>' + t('nav_shared') + '</span>';
|
| 307 |
+
try {
|
| 308 |
+
const [shRes, myRes, lkRes] = await Promise.all([fetch('/api/shared-with-me'), fetch('/api/my-shares'), fetch('/api/share-links')]);
|
| 309 |
+
const shared = await shRes.json();
|
| 310 |
+
const myShares = await myRes.json();
|
| 311 |
+
const links = await lkRes.json();
|
| 312 |
+
area.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
+
const t1 = document.createElement('div'); t1.className = 'section-title'; t1.textContent = t('sec_shared_me'); area.appendChild(t1);
|
| 315 |
+
if (!shared.length) {
|
| 316 |
+
area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:2rem 1rem;"><svg class="icon icon-lg"><use href="#icon-share"></use></svg><p>' + t('empty_shared') + '</p></div>');
|
| 317 |
+
} else {
|
| 318 |
+
const grid = document.createElement('div'); grid.className = 'grid';
|
| 319 |
+
shared.forEach(item => {
|
| 320 |
+
const el = document.createElement('div'); el.className = 'card';
|
| 321 |
+
const badge = '<span class="owner-badge"><img src="/api/users/' + item.owner_id + '/avatar" onerror="this.remove()">' + this.esc(item.owner_name) + '</span>';
|
| 322 |
+
if (item.folder_id) {
|
| 323 |
+
el.innerHTML = '<div class="card-thumb folder-thumb">' + badge + '<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>' + t('folder') + '</span><span></span></div></div>';
|
| 324 |
+
el.onclick = () => { this.currentFolder = {id: item.folder_id, name: item.folder_name}; this.loadContent(); };
|
| 325 |
+
el.oncontextmenu = (e) => this.showContextMenu(e, 'shared-folder', item);
|
| 326 |
+
this.bindLongPress(el, 'shared-folder', item);
|
| 327 |
+
} else {
|
| 328 |
+
let thumb = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
|
| 329 |
+
if (item.mime && (item.mime.startsWith('image/') || item.mime.startsWith('video/'))) thumb = '<img src="/api/files/' + item.file_id + '/thumb" alt="" loading="lazy">';
|
| 330 |
+
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});
|
| 331 |
+
el.innerHTML = '<div class="card-thumb">' + badge + thumb + '</div><div class="card-info"><div class="card-name">' + this.esc(item.file_name || '—') + '</div><div class="card-meta"><span>' + this.formatSize(item.size) + '</span><span>' + this.statusLabel(item.status) + '</span></div></div>';
|
| 332 |
+
el.onclick = () => { if (item.status === 'ready') this.previewFile(fobj); };
|
| 333 |
+
el.oncontextmenu = (e) => this.showContextMenu(e, 'shared-file', fobj);
|
| 334 |
+
this.bindLongPress(el, 'shared-file', fobj);
|
| 335 |
+
}
|
| 336 |
+
grid.appendChild(el);
|
| 337 |
+
});
|
| 338 |
+
area.appendChild(grid);
|
| 339 |
+
}
|
| 340 |
|
| 341 |
+
const t2 = document.createElement('div'); t2.className = 'section-title'; t2.textContent = t('sec_my_shares'); area.appendChild(t2);
|
| 342 |
+
if (!myShares.length) {
|
| 343 |
+
area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:1.5rem 1rem;"><p>' + t('empty_my_shares') + '</p></div>');
|
| 344 |
+
} else {
|
| 345 |
+
const list = document.createElement('div'); list.className = 'share-list';
|
| 346 |
+
myShares.forEach(sh => {
|
| 347 |
+
const name = sh.file_name || sh.folder_name || '—';
|
| 348 |
+
const kind = sh.file_id ? t('folder').replace(t('folder'), sh.file_id ? 'Fichier' : 'Dossier') : 'Dossier';
|
| 349 |
+
const item = document.createElement('div'); item.className = 'share-item';
|
| 350 |
+
item.innerHTML =
|
| 351 |
+
'<div class="share-item-header"><div class="share-item-info"><svg class="icon icon-sm"><use href="#' + (sh.file_id ? 'icon-file' : 'icon-folder') + '"></use></svg><div style="min-width:0;"><div class="share-item-name">' + this.esc(name) + '</div><div class="share-item-sub">' + (sh.file_id ? 'Fichier' : 'Dossier') + ' · ' + t('shared_with') + ' <strong style="color:var(--primary);">' + this.esc(sh.shared_with_name) + '</strong></div></div></div><button class="btn-icon myshare-revoke" data-id="' + sh.id + '"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
|
| 352 |
+
'<div class="share-item-footer">' + t('shared_on') + ' ' + this.formatDate(sh.created_at) + '</div>';
|
| 353 |
+
list.appendChild(item);
|
| 354 |
+
});
|
| 355 |
+
area.appendChild(list);
|
| 356 |
+
area.querySelectorAll('.myshare-revoke').forEach(btn => {
|
| 357 |
+
btn.onclick = async () => {
|
| 358 |
+
const ok = await this.confirm({ title: t('revoke'), message: t('dlg_remove_msg'), confirmText: t('revoke'), danger: true });
|
| 359 |
+
if (!ok) return;
|
| 360 |
+
await fetch('/api/my-shares/' + btn.dataset.id, {method:'DELETE'});
|
| 361 |
+
this.toast(t('t_revoked'), 'success');
|
| 362 |
+
this.loadSharedPage();
|
| 363 |
+
};
|
| 364 |
+
});
|
| 365 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
+
const t3 = document.createElement('div'); t3.className = 'section-title'; t3.textContent = t('sec_my_links'); area.appendChild(t3);
|
| 368 |
+
if (!links.length) {
|
| 369 |
+
area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:1.5rem 1rem;"><p>' + t('empty_links') + '</p></div>');
|
| 370 |
+
} else {
|
| 371 |
+
const grouped = {};
|
| 372 |
+
links.forEach(l => { if (!grouped[l.file_id]) grouped[l.file_id] = {name: l.file_name, links: []}; grouped[l.file_id].links.push(l); });
|
| 373 |
+
const list = document.createElement('div'); list.className = 'share-list';
|
| 374 |
+
Object.entries(grouped).forEach(([fid, data]) => {
|
| 375 |
+
const emb = data.links.find(l => l.type === 'embed');
|
| 376 |
+
const dl = data.links.find(l => l.type === 'download');
|
| 377 |
+
const exp = this.formatDate(Math.max(...data.links.map(l => l.expires_at || 0)));
|
| 378 |
+
const origin = window.location.origin;
|
| 379 |
+
const item = document.createElement('div'); item.className = 'share-item';
|
| 380 |
+
item.innerHTML =
|
| 381 |
+
'<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 || '—') + '</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>' +
|
| 382 |
+
'<div class="share-item-links">' +
|
| 383 |
+
(emb ? '<div class="share-link-row"><span class="share-link-label">' + t('ctx_preview') + '</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>' : '') +
|
| 384 |
+
(dl ? '<div class="share-link-row"><span class="share-link-label">' + t('ctx_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>' : '') +
|
| 385 |
+
'</div><div class="share-item-footer">' + t('expires') + ' ' + exp + '</div>';
|
| 386 |
+
list.appendChild(item);
|
| 387 |
+
});
|
| 388 |
+
area.appendChild(list);
|
| 389 |
+
area.querySelectorAll('.share-copy-btn').forEach(btn => {
|
| 390 |
+
btn.onclick = async () => {
|
| 391 |
+
try { await navigator.clipboard.writeText(btn.dataset.url); } catch (e) { btn.previousElementSibling.select(); document.execCommand('copy'); }
|
| 392 |
+
btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500);
|
| 393 |
+
this.toast(t('t_copied'), 'success');
|
| 394 |
+
};
|
| 395 |
+
});
|
| 396 |
+
area.querySelectorAll('.link-del-btn').forEach(btn => {
|
| 397 |
+
btn.onclick = async () => {
|
| 398 |
+
const ok = await this.confirm({ title: t('ctx_delete'), message: t('dlg_links_msg'), confirmText: t('ctx_delete'), danger: true });
|
| 399 |
+
if (!ok) return;
|
| 400 |
+
for (const tk of grouped[btn.dataset.fid].links.map(l => l.token)) await fetch('/api/share-links/' + tk, {method:'DELETE'});
|
| 401 |
+
this.toast(t('t_deleted'), 'success');
|
| 402 |
+
this.loadSharedPage();
|
| 403 |
+
};
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
} catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
|
| 407 |
+
},
|
| 408 |
|
| 409 |
+
// ✅ MULTI-UPLOAD + vitesse
|
| 410 |
+
async handleUpload(input) {
|
| 411 |
+
const files = Array.from(input.files);
|
| 412 |
+
input.value = '';
|
| 413 |
+
if (!files.length) return;
|
| 414 |
+
document.getElementById('upload-overlay').style.display = 'block';
|
| 415 |
+
let okCount = 0, errCount = 0;
|
| 416 |
+
for (let i = 0; i < files.length; i++) {
|
| 417 |
+
const result = await this.uploadOne(files[i], i + 1, files.length);
|
| 418 |
+
if (result) okCount++; else errCount++;
|
| 419 |
+
if (this._uploadCancelled) { this._uploadCancelled = false; break; }
|
| 420 |
+
}
|
| 421 |
+
document.getElementById('upload-overlay').style.display = 'none';
|
| 422 |
+
if (this.currentSection === 'files') this.loadContent();
|
| 423 |
+
if (okCount) this.toast(okCount + ' ✓' + (errCount ? ', ' + errCount + ' ✗' : ''), okCount && !errCount ? 'success' : 'error');
|
| 424 |
+
},
|
| 425 |
|
| 426 |
+
uploadOne(file, idx, total) {
|
| 427 |
+
return new Promise(async (resolve) => {
|
| 428 |
+
const t0 = performance.now();
|
| 429 |
+
document.getElementById('upload-count').textContent = '(' + idx + '/' + total + ')';
|
| 430 |
+
document.getElementById('upload-filename').textContent = file.name;
|
| 431 |
+
document.getElementById('upload-progress').style.width = '0%';
|
| 432 |
+
document.getElementById('upload-percent').textContent = '0%';
|
| 433 |
+
document.getElementById('upload-speed').textContent = '';
|
| 434 |
+
const fd = new FormData();
|
| 435 |
+
fd.append('file', file);
|
| 436 |
+
if (this.currentFolder) fd.append('folder_id', this.currentFolder.id);
|
| 437 |
+
this.uploadController = new AbortController();
|
| 438 |
+
const finish = (ok) => {
|
| 439 |
+
const secs = (performance.now() - t0) / 1000;
|
| 440 |
+
const mbps = (file.size / 1048576) / secs;
|
| 441 |
+
if (ok && isFinite(mbps)) document.getElementById('upload-speed').textContent = mbps.toFixed(2) + ' MB/s';
|
| 442 |
+
setTimeout(() => resolve(ok), 600);
|
| 443 |
+
};
|
| 444 |
+
try {
|
| 445 |
+
const res = await fetch('/api/files/upload', {method:'POST', body: fd, signal: this.uploadController.signal});
|
| 446 |
+
if (!res.ok) throw new Error('fail');
|
| 447 |
+
const d = await res.json();
|
| 448 |
+
const es = new EventSource('/api/files/' + d.fileId + '/progress');
|
| 449 |
+
es.onmessage = (e) => {
|
| 450 |
+
const data = JSON.parse(e.data);
|
| 451 |
+
if (data.type === 'progress') {
|
| 452 |
+
document.getElementById('upload-progress').style.width = Math.round(data.progress) + '%';
|
| 453 |
+
document.getElementById('upload-percent').textContent = Math.round(data.progress) + '%';
|
| 454 |
+
const secs = (performance.now() - t0) / 1000;
|
| 455 |
+
if (secs > 0.5) document.getElementById('upload-speed').textContent = ((file.size / 1048576) / secs).toFixed(2) + ' MB/s';
|
| 456 |
+
}
|
| 457 |
+
if (data.type === 'done') { es.close(); finish(true); }
|
| 458 |
+
if (data.type === 'error') { es.close(); this.toast('Erreur: ' + file.name, 'error'); finish(false); }
|
| 459 |
+
};
|
| 460 |
+
es.onerror = () => { es.close(); finish(false); };
|
| 461 |
+
} catch (e) {
|
| 462 |
+
if (e.name === 'AbortError') { this._uploadCancelled = true; resolve(false); }
|
| 463 |
+
else { this.toast('Erreur upload: ' + file.name, 'error'); finish(false); }
|
| 464 |
+
}
|
| 465 |
+
});
|
| 466 |
+
},
|
| 467 |
|
| 468 |
+
cancelUpload() { if (this.uploadController) this.uploadController.abort(); document.getElementById('upload-overlay').style.display = 'none'; },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
+
async createFolder() {
|
| 471 |
+
const name = await this.prompt({ title: t('new_folder'), placeholder: t('new_folder'), confirmText: t('new_folder') });
|
| 472 |
+
if (!name) return;
|
| 473 |
+
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})});
|
| 474 |
+
if (res.ok) { this.toast(t('t_folder_created'), 'success'); this.loadContent(); }
|
| 475 |
+
else this.toast('Erreur', 'error');
|
| 476 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
+
previewFile(file) {
|
| 479 |
+
const container = document.getElementById('preview-container');
|
| 480 |
+
container.innerHTML = '';
|
| 481 |
+
document.getElementById('preview-modal').style.display = 'flex';
|
| 482 |
+
const url = '/api/files/' + file.id + '/download';
|
| 483 |
+
document.getElementById('preview-download-btn').onclick = () => { const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); };
|
| 484 |
+
if (file.mime && file.mime.startsWith('image/')) { const i = document.createElement('img'); i.src = url; i.id = 'preview-content'; container.appendChild(i); }
|
| 485 |
+
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); }
|
| 486 |
+
else if (file.mime && file.mime.startsWith('audio/')) { const a = document.createElement('audio'); a.src = url; a.controls = true; container.appendChild(a); }
|
| 487 |
+
else container.innerHTML = '<div style="text-align:center;padding:2rem;"><p style="color:var(--text-muted);">—</p><p style="font-weight:500;margin-top:0.5rem;">' + this.esc(file.name) + '</p></div>';
|
| 488 |
+
},
|
| 489 |
+
closePreview() {
|
| 490 |
+
const c = document.getElementById('preview-container');
|
| 491 |
+
const v = c.querySelector('video'); const a = c.querySelector('audio');
|
| 492 |
+
if (v) v.pause(); if (a) a.pause();
|
| 493 |
+
c.innerHTML = '';
|
| 494 |
+
document.getElementById('preview-modal').style.display = 'none';
|
| 495 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
|
| 497 |
+
async showShareModal(file) {
|
| 498 |
+
document.getElementById('share-modal').style.display = 'flex';
|
| 499 |
+
const pi = document.getElementById('share-preview-url'), di = document.getElementById('share-download-url'), ei = document.getElementById('share-embed-code');
|
| 500 |
+
pi.value = di.value = ei.value = '...';
|
| 501 |
+
try {
|
| 502 |
+
const res = await fetch('/api/files/' + file.id + '/share', {method:'POST'});
|
| 503 |
+
if (!res.ok) throw new Error();
|
| 504 |
+
const d = await res.json();
|
| 505 |
+
const o = window.location.origin;
|
| 506 |
+
pi.value = o + d.preview;
|
| 507 |
+
di.value = o + d.download;
|
| 508 |
+
ei.value = file.mime.startsWith('image/') ? '<img src="' + o + d.download + '" alt="">' :
|
| 509 |
+
file.mime.startsWith('video/') ? '<video controls src="' + o + d.download + '"></video>' :
|
| 510 |
+
file.mime.startsWith('audio/') ? '<audio controls src="' + o + d.download + '"></audio>' :
|
| 511 |
+
'<a href="' + o + d.download + '">' + this.esc(file.name) + '</a>';
|
| 512 |
+
document.querySelectorAll('.share-copy-btn').forEach(b => { b.onclick = () => this.copyShare(b.dataset.type); });
|
| 513 |
+
} catch (e) { this.toast('Erreur', 'error'); this.closeShare(); }
|
| 514 |
+
},
|
| 515 |
+
closeShare() { document.getElementById('share-modal').style.display = 'none'; },
|
| 516 |
+
async copyShare(type) {
|
| 517 |
+
const id = type === 'preview' ? 'share-preview-url' : type === 'download' ? 'share-download-url' : 'share-embed-code';
|
| 518 |
+
const input = document.getElementById(id);
|
| 519 |
+
if (!input.value || input.value === '...') return;
|
| 520 |
+
try { await navigator.clipboard.writeText(input.value); } catch (e) { input.select(); document.execCommand('copy'); }
|
| 521 |
+
const btn = document.querySelector('.share-copy-btn[data-type="' + type + '"]');
|
| 522 |
+
if (btn) { btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }
|
| 523 |
+
this.toast(t('t_copied'), 'success');
|
| 524 |
+
},
|
| 525 |
+
|
| 526 |
+
async shareWithUser(type, item) {
|
| 527 |
+
const username = await this.prompt({ title: t('ctx_share_user'), placeholder: t('username'), confirmText: t('ctx_share_user') });
|
| 528 |
+
if (!username) return;
|
| 529 |
+
const res = await fetch('/api/share-user', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({type, id: item.id, username})});
|
| 530 |
+
const d = await res.json();
|
| 531 |
+
this.toast(d.ok ? '✓ ' + username : (d.error || 'Erreur'), d.ok ? 'success' : 'error');
|
| 532 |
+
},
|
| 533 |
+
|
| 534 |
+
showContextMenu(e, type, item) {
|
| 535 |
+
try { e.preventDefault(); } catch (err) {}
|
| 536 |
+
e.stopPropagation();
|
| 537 |
+
const menu = document.getElementById('context-menu');
|
| 538 |
+
menu.style.display = 'block';
|
| 539 |
+
if (this.isMobile) { menu.style.left = '0'; menu.style.right = '0'; menu.style.bottom = '0'; menu.style.top = 'auto'; menu.style.width = '100%'; }
|
| 540 |
+
else {
|
| 541 |
+
const cx = e.pageX || 0, cy = e.pageY || 0;
|
| 542 |
+
menu.style.left = cx + 'px'; menu.style.top = cy + 'px'; menu.style.bottom = 'auto'; menu.style.right = 'auto'; menu.style.width = 'auto';
|
| 543 |
+
const r = menu.getBoundingClientRect();
|
| 544 |
+
if (r.right > window.innerWidth) menu.style.left = (cx - r.width) + 'px';
|
| 545 |
+
if (r.bottom > window.innerHeight) menu.style.top = (cy - r.height) + 'px';
|
| 546 |
+
}
|
| 547 |
+
const isMineFile = type === 'file', isMineFolder = type === 'folder';
|
| 548 |
+
const isSharedFile = type === 'shared-file', isSharedFolder = type === 'shared-folder';
|
| 549 |
+
document.getElementById('ctx-preview').style.display = (isMineFile || isSharedFile) ? 'flex' : 'none';
|
| 550 |
+
document.getElementById('ctx-download').style.display = (isMineFile || isSharedFile) ? 'flex' : 'none';
|
| 551 |
+
document.getElementById('ctx-share').style.display = isMineFile ? 'flex' : 'none';
|
| 552 |
+
document.getElementById('ctx-share-user').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
|
| 553 |
+
document.getElementById('ctx-remove').style.display = (isSharedFile || isSharedFolder) ? 'flex' : 'none';
|
| 554 |
+
document.getElementById('ctx-delete').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
|
| 555 |
|
| 556 |
+
document.getElementById('ctx-preview').onclick = () => { this.closeMenu(); this.previewFile(item); };
|
| 557 |
+
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(); };
|
| 558 |
+
document.getElementById('ctx-share').onclick = () => { this.closeMenu(); this.showShareModal(item); };
|
| 559 |
+
document.getElementById('ctx-share-user').onclick = () => { this.closeMenu(); this.shareWithUser(isMineFile ? 'file' : 'folder', item); };
|
| 560 |
+
document.getElementById('ctx-remove').onclick = async () => {
|
| 561 |
+
this.closeMenu();
|
| 562 |
+
const ok = await this.confirm({ title: t('ctx_remove'), message: t('dlg_remove_msg'), confirmText: t('ctx_remove'), danger: true });
|
| 563 |
+
if (!ok) return;
|
| 564 |
+
await fetch('/api/shared-with-me/' + item.share_id, {method:'DELETE'});
|
| 565 |
+
this.toast(t('t_removed'), 'success');
|
| 566 |
+
this.loadSharedPage();
|
| 567 |
+
};
|
| 568 |
+
document.getElementById('ctx-delete').onclick = async () => {
|
| 569 |
+
this.closeMenu();
|
| 570 |
+
const ok = await this.confirm({ title: t('ctx_delete'), message: t('dlg_delete_msg') + ' "' + (item.name || '') + '" ?', confirmText: t('ctx_delete'), danger: true });
|
| 571 |
+
if (!ok) return;
|
| 572 |
+
await fetch('/api/' + (isMineFile ? 'files' : 'folders') + '/' + item.id, {method:'DELETE'});
|
| 573 |
+
this.toast(t('t_deleted'), 'success');
|
| 574 |
+
this.refresh();
|
| 575 |
+
};
|
| 576 |
+
},
|
| 577 |
+
closeMenu() { document.getElementById('context-menu').style.display = 'none'; }
|
| 578 |
+
};
|
| 579 |
|
| 580 |
+
app.init();
|
|
|
|
|
|
|
|
|