diff --git a/content.js b/content.js index 6391d9b..ae5d5d9 100644 --- a/content.js +++ b/content.js @@ -128,7 +128,7 @@ async function getApplicants() { function sanitizeZipPathSegment(value, fallback = "unnamed") { const sanitized = String(value ?? "") - .replace(/[\/\\:*?"<>| ]/g, "_") + .replace(/[\/\\:*?"<>|]/g, "_") .trim() return sanitized === "" ? fallback : sanitized @@ -287,46 +287,18 @@ function getFflate() { } async function createZipBlob(archiveEntries) { - const { Zip, ZipPassThrough } = getFflate() - return new Promise((resolve, reject) => { - const chunks = [] - const zip = new Zip() - let settled = false - - zip.ondata = (error, chunk, final) => { - if (settled) { - return - } - + fflate.zip(archiveEntries, { level: 0, consume: true }, (error, data) => { if (error != null) { - settled = true reject(error) return } - - chunks.push(chunk) - - if (final) { - settled = true - resolve(new Blob(chunks, { type: "application/zip" })) - } - } - - try { - Object.entries(archiveEntries).forEach(([path, bytes]) => { - const file = new ZipPassThrough(path) - - zip.add(file) - file.push(bytes, true) - }) - - zip.end() - } catch (error) { - settled = true - reject(error) - } + resolve(data) + }) }) + .then((archiveBytes) => { + return new Blob([archiveBytes], { type: "application/zip" }) + }) } function downloadBlob(blob, fileName) { @@ -378,6 +350,7 @@ function createProgressDialog() { const size = document.createElement("p") const actions = document.createElement("div") const closeButton = document.createElement("button") + const downloadButton = document.createElement("button") const entries = new Map() const progress = new Map() const startTime = Date.now() @@ -406,6 +379,12 @@ function createProgressDialog() { size.textContent = `Downloaded size: ${formatMegabytes(totalDownloadedBytes)}` } + downloadButton.type = "button" + downloadButton.id = "download-button" + downloadButton.textContent = "Download" + downloadButton.disabled = true + downloadButton.classList.add("ripper-hidden") + closeButton.type = "button" closeButton.textContent = "Close" closeButton.disabled = true @@ -420,6 +399,7 @@ function createProgressDialog() { } }) + actions.append(downloadButton) actions.append(closeButton) summary.append(elapsed, downloaded, size) dialog.append(title, status, list, summary, actions) @@ -488,6 +468,7 @@ function createProgressDialog() { // Main export flow: fetch applicant data, download files, then assemble the ZIP. function rip(event) { + Notification.requestPermission() const btn = event.target; const progressDialog = createProgressDialog() btn.textContent = "working ..." @@ -549,13 +530,20 @@ function rip(event) { archiveEntries["fflate.min.js"] = viewerFflateSource console.log("Generating zip archive...") - - return createZipBlob(archiveEntries) + createZipBlob(archiveEntries) .then((zipBlob) => { console.log("Zip archive is ready.") + if (Notification.permission == "granted") { + new Notification("EPAS Ripper", { body: `Zip archive for job offer ${aid} is ready for download` }) + } progressDialog.setStatus("Download ready.") - downloadBlob(zipBlob, `procedure_${aid}.zip`) btn.textContent = "done" + const downloadButton = document.querySelector("#download-button") + downloadButton.addEventListener("click", () => { + downloadBlob(zipBlob, `procedure_${aid}.zip`) + }) + downloadButton.classList.remove("ripper-hidden") + downloadButton.disabled = false }) }) diff --git a/manifest.json b/manifest.json index 54a2906..9424603 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,18 @@ "manifest_version": 3, "name": "KF-EPAS-Ripper", "version": "0.1.0", + "browser_specific_settings": { + "gecko": { + "id": "epas-ripper@math.janko.fr", + "data_collection_permissions": { + "required": [ + "none" + ] + } + } + }, "description": "Downloads all files from an EPAS application", + "permissions": [ "notifications" ], "host_permissions": [ "https://personal.uni-graz.at/*" ], diff --git a/style.css b/style.css index ab6f1e2..0c1d0a1 100644 --- a/style.css +++ b/style.css @@ -57,6 +57,10 @@ button.ripper-btn:disabled { max-height: 20rem; } +.ripper-hidden { + display: none; +} + .ripper-progress-item { display: flex; align-items: center; @@ -121,6 +125,7 @@ button.ripper-btn:disabled { } .ripper-progress-actions > button { + margin-left: 0.5rem; padding: 0.5rem 0.9rem; border: 1px solid #cbd5e1; border-radius: 0.5rem;