const webroot = document.querySelector("meta[name='webroot']").content;
const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone");
const convertButton = document.querySelector("input[type='submit']");
const fileNames = [];
let fileType;
let pendingFiles = 0;
let formatSelected = false;
// ─────────────────────────────────────
// Antivirus toggle UI (custom slider)
// ─────────────────────────────────────
let avToggleButton = null;
let avToggleLabel = null;
// Inject minimal CSS so the toggle looks like a real slider
function injectAvToggleStyles() {
if (document.getElementById("av-toggle-styles")) return;
const style = document.createElement("style");
style.id = "av-toggle-styles";
style.textContent = `
.av-toggle-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.av-toggle-label {
color: #04070eff;
}
.av-toggle-switch {
position: relative;
width: 42px;
height: 22px;
border-radius: 999px;
border: none;
background-color: #4b5563;
padding: 0;
cursor: pointer;
transition: background-color 0.2s ease;
display: inline-flex;
align-items: center;
}
.av-toggle-switch::before {
content: "";
position: absolute;
width: 18px;
height: 18px;
border-radius: 999px;
background-color: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.35);
left: 2px;
transition: transform 0.2s ease;
}
.av-toggle-switch.av-on {
background-color: #22c55e;
}
.av-toggle-switch.av-on::before {
transform: translateX(20px);
}
.av-toggle-switch.av-disabled {
opacity: 0.4;
cursor: not-allowed;
}
`;
document.head.appendChild(style);
}
function setAntivirusToggleVisual(enabled, available) {
if (!avToggleButton) return;
avToggleButton.classList.remove("av-on", "av-disabled");
if (!available) {
avToggleButton.classList.add("av-disabled");
avToggleButton.setAttribute("aria-disabled", "true");
avToggleButton.setAttribute("aria-pressed", "false");
if (avToggleLabel) {
avToggleLabel.textContent =
"Antivirus scan unavailable (CLAMAV_URL not set)";
}
return;
}
avToggleButton.setAttribute("aria-disabled", "false");
if (enabled) {
avToggleButton.classList.add("av-on");
avToggleButton.setAttribute("aria-pressed", "true");
} else {
avToggleButton.setAttribute("aria-pressed", "false");
}
if (avToggleLabel) {
avToggleLabel.textContent = "Enable antivirus scan";
}
}
function initAntivirusToggleState() {
if (!avToggleButton) return;
fetch(`${webroot}/api/antivirus`)
.then((res) => res.json())
.then((data) => {
console.log("Antivirus state from server:", data);
const available = !!data.available;
const enabled = !!data.enabled;
setAntivirusToggleVisual(enabled, available);
})
.catch((err) => {
console.error("Failed to get antivirus state:", err);
setAntivirusToggleVisual(false, false);
if (avToggleLabel) {
avToggleLabel.textContent = "Antivirus scan status unavailable";
}
});
avToggleButton.addEventListener("click", () => {
const isDisabled = avToggleButton.classList.contains("av-disabled");
if (isDisabled) return;
const currentlyOn = avToggleButton.classList.contains("av-on");
const newValue = !currentlyOn;
fetch(`${webroot}/api/antivirus`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: newValue }),
})
.then((res) => res.json())
.then((data) => {
console.log("Antivirus updated state:", data);
const available = !!data.available;
const enabled = !!data.enabled;
setAntivirusToggleVisual(enabled, available);
})
.catch((err) => {
console.error("Failed to update antivirus state:", err);
// On error, do not visually toggle
});
});
}
function createAntivirusToggle() {
injectAvToggleStyles();
const form = document.querySelector("form");
if (!form) return;
if (document.getElementById("av-toggle-wrapper")) return;
const wrapper = document.createElement("div");
wrapper.id = "av-toggle-wrapper";
wrapper.className = "av-toggle-wrapper";
const labelSpan = document.createElement("span");
labelSpan.id = "av-toggle-label";
labelSpan.className = "av-toggle-label";
labelSpan.textContent = "Enable antivirus scan";
const button = document.createElement("button");
button.type = "button";
button.id = "av-toggle";
button.className = "av-toggle-switch";
button.setAttribute("role", "switch");
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-disabled", "true");
wrapper.appendChild(labelSpan);
wrapper.appendChild(button);
// Insert at top of form so it's visible above dropzone
form.insertBefore(wrapper, form.firstChild);
avToggleButton = button;
avToggleLabel = labelSpan;
initAntivirusToggleState();
}
// Create the toggle as soon as script runs
createAntivirusToggle();
// ─────────────────────────────────────
// Existing upload UI logic
// ─────────────────────────────────────
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", () => {
dropZone.classList.remove("dragover");
});
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
const files = e.dataTransfer.files;
if (files.length === 0) {
console.warn("No files dropped — likely a URL or unsupported source.");
return;
}
for (const file of files) {
console.log("Handling dropped file:", file.name);
handleFile(file);
}
});
function handleFile(file) {
const fileList = document.querySelector("#file-list");
const row = document.createElement("tr");
row.innerHTML = `
${file.name} |
|
${(file.size / 1024).toFixed(2)} kB |
Remove |
`;
if (!fileType) {
fileType = file.name.split(".").pop();
fileInput.setAttribute("accept", `.${fileType}`);
setTitle();
fetch(`${webroot}/conversions`, {
method: "POST",
body: JSON.stringify({ fileType }),
headers: { "Content-Type": "application/json" },
})
.then((res) => res.text())
.then((html) => {
selectContainer.innerHTML = html;
updateSearchBar();
})
.catch(console.error);
}
fileList.appendChild(row);
file.htmlRow = row;
fileNames.push(file.name);
uploadFile(file);
}
const selectContainer = document.querySelector("form .select_container");
const updateSearchBar = () => {
const convertToInput = document.querySelector("input[name='convert_to_search']");
const convertToPopup = document.querySelector(".convert_to_popup");
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
const convertToGroups = {};
const convertToElement = document.querySelector("select[name='convert_to']");
const showMatching = (search) => {
for (const [targets, groupElement] of Object.values(convertToGroups)) {
let matchingTargetsFound = 0;
for (const target of targets) {
if (target.dataset.target.includes(search)) {
matchingTargetsFound++;
target.classList.remove("hidden");
target.classList.add("flex");
} else {
target.classList.add("hidden");
target.classList.remove("flex");
}
}
if (matchingTargetsFound === 0) {
groupElement.classList.add("hidden");
groupElement.classList.remove("flex");
} else {
groupElement.classList.remove("hidden");
groupElement.classList.add("flex");
}
}
};
for (const groupElement of convertToGroupElements) {
const groupName = groupElement.dataset.converter;
const targetElements = groupElement.querySelectorAll(".target");
const targets = Array.from(targetElements);
for (const target of targets) {
target.onmousedown = () => {
convertToElement.value = target.dataset.value;
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
formatSelected = true;
if (pendingFiles === 0 && fileNames.length > 0) {
convertButton.disabled = false;
}
showMatching("");
};
}
convertToGroups[groupName] = [targets, groupElement];
}
convertToInput.addEventListener("input", (e) => {
showMatching(e.target.value.toLowerCase());
});
convertToInput.addEventListener("search", () => {
convertButton.disabled = true;
formatSelected = false;
});
convertToInput.addEventListener("blur", (e) => {
if (e?.relatedTarget?.classList?.contains("target")) {
convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex");
return;
}
convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex");
});
convertToInput.addEventListener("focus", () => {
convertToPopup.classList.remove("hidden");
convertToPopup.classList.add("flex");
});
};
fileInput.addEventListener("change", (e) => {
const files = e.target.files;
for (const file of files) {
handleFile(file);
}
});
const setTitle = () => {
const title = document.querySelector("h1");
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const deleteRow = (target) => {
const filename = target.parentElement.parentElement.children[0].textContent;
const row = target.parentElement.parentElement;
row.remove();
const index = fileNames.indexOf(filename);
if (index !== -1) {
fileNames.splice(index, 1);
}
fileInput.value = "";
if (fileNames.length === 0) {
fileType = null;
fileInput.removeAttribute("accept");
convertButton.disabled = true;
setTitle();
}
fetch(`${webroot}/delete`, {
method: "POST",
body: JSON.stringify({ filename: filename }),
headers: {
"Content-Type": "application/json",
},
}).catch((err) => console.log(err));
};
const uploadFile = (file) => {
convertButton.disabled = true;
convertButton.textContent = "Uploading...";
pendingFiles += 1;
const formData = new FormData();
formData.append("file", file, file.name);
let xhr = new XMLHttpRequest();
xhr.open("POST", `${webroot}/upload`, true);
xhr.onload = () => {
pendingFiles -= 1;
// 🔍 1) Log exactly what the browser got
console.log("Upload raw response:", xhr.status, xhr.responseText);
let data = {};
try {
data = JSON.parse(xhr.responseText || "{}");
} catch (e) {
console.error("Failed to parse upload response:", e, xhr.responseText);
}
// 🔍 2) Compute an "infected" flag as robustly as possible
const isInfected =
(typeof data === "object" &&
data !== null &&
(data.infected === true ||
data.infected === "true" ||
(typeof data.message === "string" &&
data.message.toLowerCase().includes("infected file found")))) ||
(typeof xhr.responseText === "string" &&
xhr.responseText.toLowerCase().includes("infected file found"));
// 🔴 3) If backend reports infection, show popup and stop
if (xhr.status >= 200 && xhr.status < 300 && isInfected) {
const infectedFiles = data.infectedFiles || [];
const details = infectedFiles
.map((f) =>
`${f.name}: ${
Array.isArray(f.viruses) && f.viruses.length
? f.viruses.join(", ")
: "malware detected"
}`,
)
.join("\n");
alert(
"⚠️ Infected file found. Conversion will be aborted.\n\n" +
(details ? "Details:\n" + details : ""),
);
// Remove row for this file
if (file.htmlRow && file.htmlRow.remove) {
file.htmlRow.remove();
}
// Remove from internal list
const idx = fileNames.indexOf(file.name);
if (idx !== -1) {
fileNames.splice(idx, 1);
}
if (fileNames.length === 0) {
fileType = null;
fileInput.removeAttribute("accept");
setTitle();
convertButton.disabled = true;
} else if (pendingFiles === 0 && formatSelected) {
convertButton.disabled = false;
}
convertButton.textContent = "Convert";
const progressbar = file.htmlRow?.getElementsByTagName("progress");
if (progressbar && progressbar[0]?.parentElement) {
progressbar[0].parentElement.remove();
}
return;
}
// Generic HTTP error
if (xhr.status !== 200) {
console.error("Upload failed:", xhr.status, xhr.responseText);
alert("Upload failed. Please try again.");
convertButton.disabled = false;
convertButton.textContent = "Upload failed";
const progressbar = file.htmlRow.getElementsByTagName("progress");
if (progressbar[0]?.parentElement) {
progressbar[0].parentElement.remove();
}
return;
}
// Clean upload
if (pendingFiles === 0) {
if (formatSelected && fileNames.length > 0) {
convertButton.disabled = false;
}
convertButton.textContent = "Convert";
}
const progressbar = file.htmlRow.getElementsByTagName("progress");
if (progressbar[0]?.parentElement) {
progressbar[0].parentElement.remove();
}
console.log("Upload parsed response:", data);
};
xhr.upload.onprogress = (e) => {
let sent = e.loaded;
let total = e.total;
console.log(`upload progress (${file.name}):`, (100 * sent) / total);
let progressbar = file.htmlRow.getElementsByTagName("progress");
if (progressbar[0]) {
progressbar[0].value = (100 * sent) / total;
}
};
xhr.onerror = (e) => {
console.log("XHR error:", e);
alert("Upload failed due to a network error.");
convertButton.disabled = false;
convertButton.textContent = "Upload failed";
};
xhr.send(formData);
};
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
if (formConvert) {
formConvert.addEventListener("submit", () => {
console.log("Submitting convert form with files:", fileNames);
const hiddenInput = document.querySelector("input[name='file_names']");
if (hiddenInput) {
hiddenInput.value = JSON.stringify(fileNames);
} else {
console.warn(
"Hidden input 'file_names' not found – form will submit without it.",
);
}
});
}
updateSearchBar();