add index.html to browse the archive
This commit is contained in:
parent
ebc3b325e3
commit
2d43c159c4
2 changed files with 215 additions and 61 deletions
257
content.js
257
content.js
|
|
@ -244,6 +244,11 @@ function createApplicantsIndexHtml() {
|
|||
min-height: 100vh;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
|
|
@ -281,6 +286,25 @@ function createApplicantsIndexHtml() {
|
|||
color: #475569;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.controls p {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.controls input[type="file"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewer {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
|
@ -288,16 +312,6 @@ function createApplicantsIndexHtml() {
|
|||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 2rem;
|
||||
color: #475569;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.applicant-card {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
|
@ -343,6 +357,15 @@ function createApplicantsIndexHtml() {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.viewer-download {
|
||||
color: #0f6cbd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.viewer-download:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-list-title {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
|
|
@ -378,18 +401,33 @@ function createApplicantsIndexHtml() {
|
|||
<body>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<p id="status">Loading applicants.csv...</p>
|
||||
<section class="controls">
|
||||
<p>Select the downloaded ZIP file to browse applicants and preview files.</p>
|
||||
<input id="zip-input" type="file" accept=".zip,application/zip">
|
||||
<p id="status">Waiting for ZIP selection...</p>
|
||||
</section>
|
||||
<div id="applicants"></div>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<div class="content-header">
|
||||
<h1 id="viewer-title">Applicant archive</h1>
|
||||
<p id="viewer-subtitle">Select a file on the left to preview it here.</p>
|
||||
<p id="viewer-subtitle">Select the ZIP file, then choose a file on the left to preview it here.</p>
|
||||
<a id="viewer-download" class="viewer-download" href="#" hidden>Download current file</a>
|
||||
</div>
|
||||
<iframe id="viewer" class="viewer" title="File preview"></iframe>
|
||||
</main>
|
||||
</div>
|
||||
<script src="jszip.min.js"></script>
|
||||
<script>
|
||||
let archive = null
|
||||
let currentObjectUrl = null
|
||||
|
||||
function setStatus(message, isError) {
|
||||
const status = document.getElementById("status")
|
||||
status.textContent = message
|
||||
status.className = isError ? "error" : ""
|
||||
}
|
||||
|
||||
function parseCsv(text) {
|
||||
const rows = []
|
||||
let row = []
|
||||
|
|
@ -438,11 +476,23 @@ function createApplicantsIndexHtml() {
|
|||
return value || "—"
|
||||
}
|
||||
|
||||
function encodeArchivePath(path) {
|
||||
return String(path || "")
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
function guessMimeType(fileName) {
|
||||
const extension = String(fileName || "").split(".").pop().toLowerCase()
|
||||
const mimeTypes = {
|
||||
csv: "text/csv;charset=utf-8",
|
||||
gif: "image/gif",
|
||||
htm: "text/html;charset=utf-8",
|
||||
html: "text/html;charset=utf-8",
|
||||
jpeg: "image/jpeg",
|
||||
jpg: "image/jpeg",
|
||||
pdf: "application/pdf",
|
||||
png: "image/png",
|
||||
svg: "image/svg+xml",
|
||||
txt: "text/plain;charset=utf-8",
|
||||
webp: "image/webp"
|
||||
}
|
||||
|
||||
return mimeTypes[extension] || "application/octet-stream"
|
||||
}
|
||||
|
||||
function getDisplayTitle(record) {
|
||||
|
|
@ -467,26 +517,63 @@ function createApplicantsIndexHtml() {
|
|||
}))
|
||||
}
|
||||
|
||||
function setViewer(file, link) {
|
||||
function resetViewer(message) {
|
||||
const viewer = document.getElementById("viewer")
|
||||
const viewerTitle = document.getElementById("viewer-title")
|
||||
const viewerSubtitle = document.getElementById("viewer-subtitle")
|
||||
const viewerDownload = document.getElementById("viewer-download")
|
||||
|
||||
if (currentObjectUrl != null) {
|
||||
URL.revokeObjectURL(currentObjectUrl)
|
||||
currentObjectUrl = null
|
||||
}
|
||||
|
||||
document.querySelectorAll(".file-link.active").forEach((element) => {
|
||||
element.classList.remove("active")
|
||||
})
|
||||
|
||||
if (link) {
|
||||
link.classList.add("active")
|
||||
viewer.removeAttribute("src")
|
||||
viewerTitle.textContent = "Applicant archive"
|
||||
viewerSubtitle.textContent = message
|
||||
viewerDownload.hidden = true
|
||||
viewerDownload.removeAttribute("href")
|
||||
viewerDownload.removeAttribute("download")
|
||||
}
|
||||
|
||||
async function setViewer(file, link) {
|
||||
if (archive == null) {
|
||||
throw new Error("No archive loaded")
|
||||
}
|
||||
|
||||
const zipEntry = archive.file(file.relativePath)
|
||||
const viewer = document.getElementById("viewer")
|
||||
const viewerTitle = document.getElementById("viewer-title")
|
||||
const viewerSubtitle = document.getElementById("viewer-subtitle")
|
||||
const encodedPath = encodeArchivePath(file.relativePath)
|
||||
const subtitleLink = document.createElement("a")
|
||||
const viewerDownload = document.getElementById("viewer-download")
|
||||
|
||||
viewer.src = encodedPath
|
||||
if (zipEntry == null) {
|
||||
throw new Error("Missing archive entry: " + file.relativePath)
|
||||
}
|
||||
|
||||
const blob = await zipEntry.async("blob")
|
||||
const previewBlob = blob.type === "" ? new Blob([blob], { type: guessMimeType(file.fileName) }) : blob
|
||||
|
||||
if (currentObjectUrl != null) {
|
||||
URL.revokeObjectURL(currentObjectUrl)
|
||||
}
|
||||
|
||||
currentObjectUrl = URL.createObjectURL(previewBlob)
|
||||
viewerTitle.textContent = file.fileName
|
||||
subtitleLink.href = encodedPath
|
||||
subtitleLink.textContent = file.relativePath
|
||||
viewerSubtitle.replaceChildren("Previewing ", subtitleLink)
|
||||
viewerSubtitle.textContent = file.relativePath
|
||||
viewer.src = currentObjectUrl
|
||||
viewerDownload.href = currentObjectUrl
|
||||
viewerDownload.download = file.fileName
|
||||
viewerDownload.hidden = false
|
||||
|
||||
document.querySelectorAll(".file-link.active").forEach((element) => {
|
||||
element.classList.remove("active")
|
||||
})
|
||||
link.classList.add("active")
|
||||
}
|
||||
|
||||
function createInfoRow(label, value) {
|
||||
|
|
@ -501,11 +588,7 @@ function createApplicantsIndexHtml() {
|
|||
|
||||
function renderApplicants(applicants) {
|
||||
const container = document.getElementById("applicants")
|
||||
const status = document.getElementById("status")
|
||||
container.replaceChildren()
|
||||
status.textContent = applicants.length + " applicants"
|
||||
|
||||
let firstFileLink = null
|
||||
|
||||
applicants.forEach((applicant) => {
|
||||
const card = document.createElement("section")
|
||||
|
|
@ -541,16 +624,16 @@ function createApplicantsIndexHtml() {
|
|||
const link = document.createElement("a")
|
||||
|
||||
link.className = "file-link"
|
||||
link.href = encodeArchivePath(file.relativePath)
|
||||
link.href = "#"
|
||||
link.textContent = file.fileName
|
||||
link.addEventListener("click", (event) => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault()
|
||||
setViewer(file, link)
|
||||
})
|
||||
|
||||
if (firstFileLink == null) {
|
||||
firstFileLink = { file, link }
|
||||
try {
|
||||
await setViewer(file, link)
|
||||
} catch (error) {
|
||||
setStatus(error.message, true)
|
||||
}
|
||||
})
|
||||
|
||||
item.append(link)
|
||||
fileList.append(item)
|
||||
|
|
@ -565,35 +648,83 @@ function createApplicantsIndexHtml() {
|
|||
card.append(heading, title, infoList, detailsLink, fileListTitle, fileList)
|
||||
container.append(card)
|
||||
})
|
||||
|
||||
if (firstFileLink != null) {
|
||||
setViewer(firstFileLink.file, firstFileLink.link)
|
||||
}
|
||||
}
|
||||
|
||||
fetch("applicants.csv")
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Unable to read applicants.csv")
|
||||
async function loadArchive(file) {
|
||||
if (typeof JSZip === "undefined") {
|
||||
throw new Error("JSZip is not available")
|
||||
}
|
||||
return response.text()
|
||||
})
|
||||
.then((text) => {
|
||||
|
||||
setStatus("Loading " + file.name + "...", false)
|
||||
const zip = await JSZip.loadAsync(file)
|
||||
const csvEntry = zip.file("applicants.csv")
|
||||
|
||||
if (csvEntry == null) {
|
||||
throw new Error("Archive does not contain applicants.csv")
|
||||
}
|
||||
|
||||
const text = await csvEntry.async("string")
|
||||
archive = zip
|
||||
resetViewer("Select a file on the left to preview it here.")
|
||||
|
||||
if (text.trim() === "") {
|
||||
renderApplicants([])
|
||||
setStatus("applicants.csv is empty", false)
|
||||
return
|
||||
}
|
||||
|
||||
const rows = parseCsv(text)
|
||||
const [header, ...records] = rows
|
||||
|
||||
if (header == null) {
|
||||
throw new Error("Unable to parse applicants.csv")
|
||||
}
|
||||
|
||||
const applicants = readApplicants(records.map((row) => Object.fromEntries(header.map((key, index) => [key, row[index] ?? ""]))))
|
||||
renderApplicants(applicants)
|
||||
setStatus("Loaded " + applicants.length + " applicants from " + file.name, false)
|
||||
}
|
||||
|
||||
document.getElementById("zip-input").addEventListener("change", async (event) => {
|
||||
const file = event.target.files && event.target.files[0]
|
||||
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await loadArchive(file)
|
||||
} catch (error) {
|
||||
archive = null
|
||||
document.getElementById("applicants").replaceChildren()
|
||||
resetViewer("Unable to load the selected ZIP file.")
|
||||
setStatus(error.message, true)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const status = document.getElementById("status")
|
||||
status.className = "error"
|
||||
status.textContent = error.message
|
||||
})
|
||||
|
||||
resetViewer("Select the ZIP file, then choose a file on the left to preview it here.")
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
async function getArchiveViewerJsZipSource() {
|
||||
const candidates = [
|
||||
"lib/jszip.min.js",
|
||||
"lib/jszip.js"
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const response = await fetch(chrome.runtime.getURL(candidate))
|
||||
|
||||
if (response.ok) {
|
||||
return response.text()
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Failed to load archive viewer dependency")
|
||||
}
|
||||
|
||||
function downloadBlob(blob, fileName) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
|
|
@ -650,7 +781,7 @@ function createProgressDialog() {
|
|||
dialog.classList.add("ripper-progress-dialog")
|
||||
title.textContent = "EPAS Ripper"
|
||||
status.classList.add("ripper-progress-status")
|
||||
status.textContent = "Retrieving applicants list (will take some time)..."
|
||||
status.textContent = "Retrieving applicants list (will take several minutes)..."
|
||||
list.classList.add("ripper-progress-list")
|
||||
summary.classList.add("ripper-progress-summary")
|
||||
elapsed.classList.add("ripper-progress-elapsed")
|
||||
|
|
@ -759,7 +890,7 @@ function rip(event) {
|
|||
btn.disabled = true;
|
||||
getApplicants()
|
||||
.then(async ({ aid, applicants, token }) => {
|
||||
progressDialog.setStatus("Downloading applicant files...")
|
||||
progressDialog.setStatus("Downloading applicant files (will take a lot of time)...")
|
||||
applicants.forEach((applicant) => {
|
||||
progressDialog.initializeApplicant(applicant)
|
||||
})
|
||||
|
|
@ -776,7 +907,8 @@ function rip(event) {
|
|||
}
|
||||
}))
|
||||
|
||||
progressDialog.setStatus("Creating zip archive...")
|
||||
console.log("Preparing zip archive...")
|
||||
progressDialog.setStatus("Preparing zip archive...")
|
||||
const zip = new JSZip()
|
||||
const successfulApplicants = []
|
||||
|
||||
|
|
@ -798,9 +930,20 @@ function rip(event) {
|
|||
})
|
||||
})
|
||||
|
||||
console.log("Creating applicants.csv")
|
||||
zip.file("applicants.csv", createApplicantsCsv(successfulApplicants))
|
||||
|
||||
console.log("Creating index.html")
|
||||
zip.file("index.html", createApplicantsIndexHtml())
|
||||
|
||||
console.log("Adding viewer Javascript to archive")
|
||||
zip.file("jszip.min.js", await getArchiveViewerJsZipSource())
|
||||
|
||||
|
||||
console.log("Generating zip archive...")
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" })
|
||||
|
||||
console.log("Zip archive is ready.")
|
||||
progressDialog.setStatus("Download ready.")
|
||||
downloadBlob(zipBlob, `applications_${aid}.zip`)
|
||||
btn.textContent = "done"
|
||||
|
|
@ -822,7 +965,7 @@ function rip(event) {
|
|||
function install() {
|
||||
const titleTag = document.querySelector("scrm-module-title")
|
||||
|
||||
if (titleTag != null) {
|
||||
if (titleTag != null && window.location.hash.startsWith("#/job-procedures/record")) {
|
||||
if (titleTag.querySelector(".ripper-btn") != null) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,16 @@
|
|||
"style.css"
|
||||
]
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"lib/jszip.js",
|
||||
"lib/jszip.min.js"
|
||||
],
|
||||
"matches": [
|
||||
"https://personal.uni-graz.at/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue