"use strict"; (function () { const $ = (sel) => document.querySelector(sel); const $$ = (sel) => Array.from(document.querySelectorAll(sel)); const dropZone = $("#dropZone"); const fileInput = $("#fileInput"); const searchInput = $("#searchInput"); const clearBtn = $("#clearBtn"); const downloadAllBtn = $("#downloadAllBtn"); const statusEl = $("#status"); const progressEl = $("#progress"); const progressBar = $("#progressBar"); const fileNameEl = $("#fileName"); const fileSizeEl = $("#fileSize"); const fileListEl = $("#fileList"); const countEl = $("#count"); const previewSection = $("#previewSection"); const previewContent = $("#previewContent"); const closePreviewBtn = $("#closePreviewBtn"); const state = { zip: null, entries: [], // { name, file, size, type, lastModDate, url? } objectUrls: [], totalBytes: 0, loadedBytes: 0, currentZipFile: null }; // Utils function bytesToSize(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); const val = parseFloat((bytes / Math.pow(k, i)).toFixed(2)); return `${val} ${sizes[i]}`; } function sanitizeName(name) { // Avoid path traversal and invalid characters const cleaned = name.replace(/^(\.*\/)+/, "").replace(/\.\./g, ""); return cleaned || "file"; } function setStatus(text, type = "info") { statusEl.textContent = text; statusEl.className = "status"; if (type === "error") { statusEl.style.color = "#ef4444"; } else if (type === "success") { statusEl.style.color = "#22c55e"; } else { statusEl.style.color = "var(--muted)"; } } function setProgress(percent) { if (percent <= 0 || percent >= 100) { progressEl.style.display = "none"; progressEl.setAttribute("aria-hidden", "true"); } else { progressEl.style.display = "block"; progressEl.setAttribute("aria-hidden", "false"); } progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`; } function clearObjectUrls() { state.objectUrls.forEach((url) => URL.revokeObjectURL(url)); state.objectUrls = []; } function resetAll() { state.zip = null; state.entries = []; state.totalBytes = 0; state.loadedBytes = 0; state.currentZipFile = null; clearObjectUrls(); fileListEl.innerHTML = ""; countEl.textContent = "0"; fileNameEl.textContent = ""; fileSizeEl.textContent = ""; setStatus("No file loaded."); setProgress(0); clearBtn.disabled = true; downloadAllBtn.disabled = true; hidePreview(); } function hidePreview() { previewSection.hidden = true; previewContent.innerHTML = ""; } function showPreviewFor(entry) { previewSection.hidden = false; previewContent.innerHTML = ""; const type = (entry.type || "").toLowerCase(); const name = entry.name; // Text preview if (type.startsWith("text/") || /\.(txt|md|csv|json|js|ts|jsx|tsx|html|css|scss|sass|less|yml|yaml|xml|log)$/i.test(name)) { const pre = document.createElement("pre"); pre.textContent = "Loading preview..."; entry.file.text().then((txt) => { pre.textContent = txt; }).catch(() => { pre.textContent = "Unable to preview this text file."; }); previewContent.appendChild(pre); return; } // Image preview if (type.startsWith("image/") || /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(name)) { const img = document.createElement("img"); img.alt = name; const url = URL.createObjectURL(entry.file); state.objectUrls.push(url); img.src = url; img.onload = () => { // Keep object URL for preview lifetime; will be revoked on clear/reset }; previewContent.appendChild(img); return; } // PDF preview if (type === "application/pdf" || /\.pdf$/i.test(name)) { const embed = document.createElement("embed"); embed.type = "application/pdf"; const url = URL.createObjectURL(entry.file); state.objectUrls.push(url); embed.src = url; previewContent.appendChild(embed); return; } // Fallback message const p = document.createElement("p"); p.textContent = "Preview is not available for this file type."; previewContent.appendChild(p); } function renderList(entries) { fileListEl.innerHTML = ""; const frag = document.createDocumentFragment(); entries.forEach((entry) => { const li = document.createElement("li"); li.className = "file"; const nameCol = document.createElement("div"); nameCol.className = "file__name"; const badge = document.createElement("span"); badge.className = "badge"; badge.textContent = entry.type || "file"; const path = document.createElement("span"); path.className = "path"; path.textContent = entry.name; nameCol.appendChild(badge); nameCol.appendChild(path); const size = document.createElement("div"); size.className = "file__size"; size.textContent = bytesToSize(entry.size); const actions = document.createElement("div"); actions.className = "file__actions"; const previewBtn = document.createElement("button"); previewBtn.className = "btn btn-secondary"; previewBtn.textContent = "Preview"; previewBtn.addEventListener("click", () => showPreviewFor(entry)); const downloadBtn = document.createElement("button"); downloadBtn.className = "btn btn-primary"; downloadBtn.textContent = "Download"; downloadBtn.addEventListener("click", () => { const a = document.createElement("a"); const url = URL.createObjectURL(entry.file); state.objectUrls.push(url); a.href = url; a.download = entry.name.split("/").pop(); document.body.appendChild(a); a.click(); a.remove(); }); actions.appendChild(previewBtn); actions.appendChild(downloadBtn); li.appendChild(nameCol); li.appendChild(size); li.appendChild(actions); frag.appendChild(li); }); fileListEl.appendChild(frag); countEl.textContent = String(entries.length); } async function handleZipFile(file) { resetAll(); state.currentZipFile = file; fileNameEl.textContent = file.name || "ZIP archive"; fileSizeEl.textContent = bytesToSize(file.size); setStatus("Loading ZIP..."); setProgress(2); try { const zip = await JSZip.loadAsync(file); state.zip = zip; const entries = []; let processed = 0; const total = Object.keys(zip.files).length; for (const [path, entry] of Object.entries(zip.files)) { // Skip directories if (entry.dir) { processed++; setProgress(Math.round((processed / total) * 100)); continue; } const safeName = sanitizeName(path); // Obtain as Blob const blob = await entry.async("blob"); const fileObj = new File([blob], safeName, { type: blob.type || "application/octet-stream", lastModified: entry.date?.getTime?.() || Date.now() }); entries.push({ name: safeName, file: fileObj, size: fileObj.size, type: fileObj.type || "application/octet-stream", lastModDate: entry.date }); processed++; if (processed % 5 === 0) { setProgress(Math.round((processed / total) * 100)); await new Promise((r) => setTimeout(r, 0)); // Yield to UI } } // Sort by path then size desc for consistent order entries.sort((a, b) => a.name.localeCompare(b.name) || b.size - a.size); state.entries = entries; state.totalBytes = entries.reduce((acc, e) => acc + e.size, 0); state.loadedBytes = state.totalBytes; renderList(entries); setStatus(`Loaded ${entries.length} files from "${file.name}".`); setProgress(100); clearBtn.disabled = false; downloadAllBtn.disabled = entries.length === 0; } catch (err) { console.error(err); setStatus("Failed to read the ZIP file. It may be corrupted or unsupported.", "error"); setProgress(0); } } function filterEntries(query) { const q = query.trim().toLowerCase(); if (!q) return state.entries; return state.entries.filter((e) => e.name.toLowerCase().includes(q)); } async function downloadAll() { if (!state.entries.length) return; const zip = new JSZip(); for (const e of state.entries) { zip.file(e.name, e.file); } setStatus("Preparing ZIP for download..."); setProgress(3); try { const blob = await zip.generateAsync( { type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 } }, (metadata) => { setProgress(Math.max(3, Math.min(99, Math.round(metadata.percent)))); } ); const a = document.createElement("a"); const url = URL.createObjectURL(blob); a.href = url; a.download = (state.currentZipFile?.name?.replace(/\.zip$/i, "") || "extracted") + "-extracted.zip"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); setStatus("ZIP ready for download.", "success"); setProgress(100); } catch (err) { console.error(err); setStatus("Failed to create ZIP for download.", "error"); setProgress(0); } } // Event handlers dropZone.addEventListener("click", () => fileInput.click()); dropZone.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fileInput.click(); } }); ["dragenter", "dragover"].forEach((evt) => { dropZone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add("is-dragover"); }); }); ["dragleave", "drop"].forEach((evt) => { dropZone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); if (evt === "drop") { const dt = e.dataTransfer; const file = dt?.files?.[0]; if (file) handleZipFile(file); } dropZone.classList.remove("is-dragover"); }); }); fileInput.addEventListener("change", () => { const file = fileInput.files?.[0]; if (file) handleZipFile(file); }); searchInput.addEventListener("input", () => { renderList(filterEntries(searchInput.value)); }); clearBtn.addEventListener("click", () => { fileInput.value = ""; resetAll(); }); downloadAllBtn.addEventListener("click", () => { downloadAll(); }); closePreviewBtn.addEventListener("click", () => { hidePreview(); }); // Init resetAll(); })();