|
156 | 156 | background: #005fa3; |
157 | 157 | } |
158 | 158 |
|
| 159 | +.file-list { |
| 160 | + margin: 8px 0 14px 0; |
| 161 | + padding: 0; |
| 162 | + list-style: none; |
| 163 | + font-size: 14px; |
| 164 | +} |
| 165 | +.file-list li { |
| 166 | + margin: 0 0 3px 0; |
| 167 | + padding: 2px 0 2px 16px; |
| 168 | + background: url('data:image/svg+xml;utf8,<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="5" fill="%2388c" /></svg>') 0 6px no-repeat; |
| 169 | + background-size: 10px 10px; |
| 170 | + color: #444; |
| 171 | + word-break: break-all; |
| 172 | +} |
| 173 | + |
159 | 174 | @media (max-width: 600px) { |
160 | 175 | .tab-content { |
161 | 176 | padding: 16px 4vw; |
|
193 | 208 | Plaintext: |
194 | 209 | <textarea id="plaintext"></textarea> |
195 | 210 | <button type="button" onclick="encrypt()">Encrypt</button> |
196 | | - <input type="file" id="fileInput" style="display:none" /> |
197 | | - <button type="button" onclick="document.getElementById('fileInput').click()">Attach File</button> |
198 | | - <button type="button" id="downloadAttachedBtn" style="display:none" onclick="downloadAttachedFile()">Download Attached File</button> |
| 211 | + <input type="file" id="fileInput" style="display:none" multiple /> |
| 212 | + <button type="button" onclick="document.getElementById('fileInput').click()">Attach File(s)</button> |
| 213 | + <ul class="file-list" id="attachedFilesList"></ul> |
| 214 | + <button type="button" id="downloadAttachedBtn" style="display:none" onclick="downloadAttachedFile()">Download Attached File(s)</button> |
199 | 215 | </div> |
200 | 216 | <div id="tab2" class="tab-content"> |
201 | 217 | Ciphertext: |
|
237 | 253 | return sha256(multiplied.toString()); |
238 | 254 | } |
239 | 255 |
|
240 | | - let attachedFile = null; |
241 | | - let attachedFilename = ""; |
242 | | - let lastDecryptedFile = null; |
243 | | - let lastDecryptedFilename = ""; |
| 256 | + let attachedFiles = []; |
| 257 | + let lastDecryptedFiles = []; |
244 | 258 |
|
245 | 259 | document.getElementById('fileInput').addEventListener('change', function(evt) { |
246 | | - if (evt.target.files.length > 0) { |
247 | | - attachedFile = evt.target.files[0]; |
248 | | - attachedFilename = attachedFile.name; |
249 | | - document.getElementById('downloadAttachedBtn').style.display = "none"; |
250 | | - } else { |
251 | | - attachedFile = null; |
252 | | - attachedFilename = ""; |
253 | | - } |
| 260 | + const files = Array.from(evt.target.files); |
| 261 | + attachedFiles = files; |
| 262 | + updateAttachedFilesList(); |
| 263 | + document.getElementById('downloadAttachedBtn').style.display = "none"; |
254 | 264 | }); |
255 | 265 |
|
| 266 | + function updateAttachedFilesList() { |
| 267 | + const list = document.getElementById('attachedFilesList'); |
| 268 | + list.innerHTML = ""; |
| 269 | + if (attachedFiles.length > 0) { |
| 270 | + attachedFiles.forEach(f => { |
| 271 | + const li = document.createElement('li'); |
| 272 | + li.textContent = f.name; |
| 273 | + list.appendChild(li); |
| 274 | + }); |
| 275 | + } |
| 276 | + } |
| 277 | + |
256 | 278 | function fileToBase64(file) { |
257 | 279 | return new Promise((resolve, reject) => { |
258 | 280 | const reader = new FileReader(); |
|
281 | 303 | document.getElementById('num').value = num; |
282 | 304 | } |
283 | 305 | let envelope = null; |
284 | | - if (attachedFile) { |
285 | | - const fileDataB64 = await fileToBase64(attachedFile); |
| 306 | + if (attachedFiles.length > 0) { |
| 307 | + const filesArr = []; |
| 308 | + for (const file of attachedFiles) { |
| 309 | + const fileDataB64 = await fileToBase64(file); |
| 310 | + filesArr.push({ |
| 311 | + n: file.name, |
| 312 | + d: fileDataB64 |
| 313 | + }); |
| 314 | + } |
286 | 315 | envelope = JSON.stringify({ |
287 | 316 | t: plaintext, |
288 | | - f: attachedFilename, |
289 | | - d: fileDataB64 |
| 317 | + files: filesArr |
290 | 318 | }); |
291 | 319 | } else { |
292 | 320 | envelope = JSON.stringify({ t: plaintext }); |
|
299 | 327 | ); |
300 | 328 | const full = new Uint8Array([...iv, ...new Uint8Array(ctBuf)]); |
301 | 329 | document.getElementById('ciphertext').value = bufToB64(full); |
302 | | - attachedFile = null; |
303 | | - attachedFilename = ""; |
| 330 | + attachedFiles = []; |
| 331 | + updateAttachedFilesList(); |
304 | 332 | document.getElementById('fileInput').value = ""; |
305 | 333 | document.getElementById('downloadAttachedBtn').style.display = "none"; |
306 | 334 | showTabContent('tab2'); |
|
328 | 356 | throw new Error('Corrupted decrypted data'); |
329 | 357 | } |
330 | 358 | document.getElementById('plaintext').value = envelope.t || ""; |
331 | | - if (envelope.f && envelope.d) { |
332 | | - lastDecryptedFile = base64ToUint8Array(envelope.d); |
333 | | - lastDecryptedFilename = envelope.f; |
| 359 | + lastDecryptedFiles = []; |
| 360 | + if (envelope.files && Array.isArray(envelope.files) && envelope.files.length > 0) { |
| 361 | + lastDecryptedFiles = envelope.files.map(f => ({ |
| 362 | + name: f.n, |
| 363 | + data: base64ToUint8Array(f.d) |
| 364 | + })); |
334 | 365 | document.getElementById('downloadAttachedBtn').style.display = "inline"; |
335 | 366 | } else { |
336 | | - lastDecryptedFile = null; |
337 | | - lastDecryptedFilename = ""; |
338 | 367 | document.getElementById('downloadAttachedBtn').style.display = "none"; |
| 368 | + lastDecryptedFiles = []; |
339 | 369 | } |
340 | 370 | showTabContent('tab1'); |
341 | 371 | } catch { |
|
344 | 374 | } |
345 | 375 |
|
346 | 376 | function downloadAttachedFile() { |
347 | | - if (!lastDecryptedFile || !lastDecryptedFilename) return; |
348 | | - const blob = new Blob([lastDecryptedFile]); |
349 | | - const url = URL.createObjectURL(blob); |
350 | | - const a = document.createElement('a'); |
351 | | - a.href = url; |
352 | | - a.download = lastDecryptedFilename; |
353 | | - document.body.appendChild(a); |
354 | | - a.click(); |
355 | | - setTimeout(() => { |
356 | | - document.body.removeChild(a); |
357 | | - URL.revokeObjectURL(url); |
358 | | - }, 100); |
| 377 | + if (!lastDecryptedFiles.length) return; |
| 378 | + if (lastDecryptedFiles.length === 1) { |
| 379 | + const blob = new Blob([lastDecryptedFiles[0].data]); |
| 380 | + const url = URL.createObjectURL(blob); |
| 381 | + const a = document.createElement('a'); |
| 382 | + a.href = url; |
| 383 | + a.download = lastDecryptedFiles[0].name; |
| 384 | + document.body.appendChild(a); |
| 385 | + a.click(); |
| 386 | + setTimeout(() => { |
| 387 | + document.body.removeChild(a); |
| 388 | + URL.revokeObjectURL(url); |
| 389 | + }, 100); |
| 390 | + } else { |
| 391 | + const zipName = "attachments.zip"; |
| 392 | + createAndDownloadZip(lastDecryptedFiles, zipName); |
| 393 | + } |
| 394 | + } |
| 395 | + |
| 396 | + function createAndDownloadZip(files, zipName) { |
| 397 | + if (typeof JSZip === "undefined") { |
| 398 | + alert("Multiple file download requires JSZip library."); |
| 399 | + return; |
| 400 | + } |
| 401 | + const zip = new JSZip(); |
| 402 | + files.forEach(f => zip.file(f.name, f.data)); |
| 403 | + zip.generateAsync({ type: "blob" }).then(blob => { |
| 404 | + const url = URL.createObjectURL(blob); |
| 405 | + const a = document.createElement('a'); |
| 406 | + a.href = url; |
| 407 | + a.download = zipName; |
| 408 | + document.body.appendChild(a); |
| 409 | + a.click(); |
| 410 | + setTimeout(() => { |
| 411 | + document.body.removeChild(a); |
| 412 | + URL.revokeObjectURL(url); |
| 413 | + }, 100); |
| 414 | + }); |
359 | 415 | } |
360 | 416 |
|
361 | 417 | function showTabContent(tabId) { |
|
370 | 426 | } |
371 | 427 | } |
372 | 428 | </script> |
| 429 | +<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> |
373 | 430 | </html> |
0 commit comments