Files
2026-08-14 11:44:00 +02:00

936 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const encodeForm =
document.getElementById("encode-form");
const decodeForm =
document.getElementById("decode-form");
const encodeImage =
document.getElementById("encode-image");
const decodeImage =
document.getElementById("decode-image");
const encodeDropzone =
document.getElementById("encode-dropzone");
const decodeDropzone =
document.getElementById("decode-dropzone");
const encodeImageInfo =
document.getElementById("encode-image-info");
const decodeImageInfo =
document.getElementById("decode-image-info");
const messageSection =
document.getElementById("message-section");
const fileSection =
document.getElementById("file-section");
const messageInput =
document.getElementById("message");
const messageSize =
document.getElementById("message-size");
const secretFile =
document.getElementById("secret-file");
const encodePassword =
document.getElementById("encode-password");
const decodePassword =
document.getElementById("decode-password");
const encodeButton =
document.getElementById("encode-button");
const decodeButton =
document.getElementById("decode-button");
const encodeStatus =
document.getElementById("encode-status");
const decodeStatus =
document.getElementById("decode-status");
const decodedMessage =
document.getElementById("decoded-message");
const messageOutput =
document.getElementById("message-output");
const copyMessage =
document.getElementById("copy-message");
function getEncodeMode() {
const selected =
document.querySelector(
'input[name="encode-mode"]:checked'
);
return selected
? selected.value
: "message";
}
function setStatus(
element,
message,
type = ""
) {
element.textContent =
message;
element.className =
`status ${type}`;
}
function setLoading(
button,
loading
) {
if (loading) {
button.disabled =
true;
button.dataset.originalText =
button.textContent;
button.textContent =
"Working...";
} else {
button.disabled =
false;
button.textContent =
button.dataset.originalText ||
button.textContent;
}
}
function formatBytes(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function setSelectedFile(
input,
file
) {
/*
* DataTransfer lets us put a file selected
* through drag-and-drop into the file input.
*/
const dataTransfer =
new DataTransfer();
dataTransfer.items.add(file);
input.files =
dataTransfer.files;
input.dispatchEvent(
new Event(
"change",
{
bubbles: true
}
)
);
}
function setupDropzone(
dropzone,
input
) {
dropzone.addEventListener(
"click",
() => {
input.click();
}
);
dropzone.addEventListener(
"keydown",
(event) => {
if (
event.key === "Enter" ||
event.key === " "
) {
event.preventDefault();
input.click();
}
}
);
[
"dragenter",
"dragover"
].forEach((eventName) => {
dropzone.addEventListener(
eventName,
(event) => {
event.preventDefault();
dropzone.classList.add(
"dragging"
);
}
);
});
[
"dragleave",
"drop"
].forEach((eventName) => {
dropzone.addEventListener(
eventName,
(event) => {
event.preventDefault();
dropzone.classList.remove(
"dragging"
);
}
);
});
dropzone.addEventListener(
"drop",
(event) => {
const file =
event.dataTransfer.files[0];
if (!file) {
return;
}
if (
!file.type.includes("png") &&
!file.name
.toLowerCase()
.endsWith(".png")
) {
alert(
"Please select a PNG image."
);
return;
}
setSelectedFile(
input,
file
);
}
);
input.addEventListener(
"change",
() => {
const file =
input.files[0];
if (!file) {
return;
}
dropzone.classList.add(
"has-file"
);
const strong =
dropzone.querySelector(
"strong"
);
const span =
dropzone.querySelector(
"span"
);
strong.textContent =
file.name;
span.textContent =
formatBytes(file.size);
}
);
}
setupDropzone(
encodeDropzone,
encodeImage
);
setupDropzone(
decodeDropzone,
decodeImage
);
async function getImageInfo(file) {
const form =
new FormData();
form.append(
"image",
file
);
const response =
await fetch(
"/api/info",
{
method: "POST",
body: form
}
);
const data =
await response.json();
if (!response.ok) {
throw new Error(
data.error ||
"Unable to inspect image"
);
}
return data;
}
async function updateImageInfo(
file,
element,
statusElement
) {
element.classList.add(
"hidden"
);
if (!file) {
return;
}
try {
const info =
await getImageInfo(file);
element.innerHTML =
`
<strong>
${info.width} × ${info.height}
</strong>
<span>
${info.format.toUpperCase()}
· ${info.capacityKB} KB capacity
</span>
`;
element.classList.remove(
"hidden"
);
} catch (error) {
setStatus(
statusElement,
error.message,
"error"
);
}
}
encodeImage.addEventListener(
"change",
async () => {
await updateImageInfo(
encodeImage.files[0],
encodeImageInfo,
encodeStatus
);
}
);
messageInput.addEventListener(
"input",
() => {
const count =
messageInput.value.length;
messageSize.textContent =
`${count.toLocaleString()} character${
count === 1
? ""
: "s"
}`;
}
);
document
.querySelectorAll(
'input[name="encode-mode"]'
)
.forEach((radio) => {
radio.addEventListener(
"change",
() => {
const mode =
getEncodeMode();
if (
mode === "message"
) {
messageSection
.classList
.remove("hidden");
fileSection
.classList
.add("hidden");
} else {
messageSection
.classList
.add("hidden");
fileSection
.classList
.remove("hidden");
}
}
);
});
encodeForm.addEventListener(
"submit",
async (event) => {
event.preventDefault();
const image =
encodeImage.files[0];
const password =
encodePassword.value;
const mode =
getEncodeMode();
if (!image) {
setStatus(
encodeStatus,
"Please select a PNG image.",
"error"
);
return;
}
const form =
new FormData();
form.append(
"image",
image
);
/*
* Password is optional.
*/
if (password) {
form.append(
"password",
password
);
}
if (
mode === "message"
) {
if (
!messageInput.value
) {
setStatus(
encodeStatus,
"Please enter a message.",
"error"
);
return;
}
form.append(
"message",
messageInput.value
);
} else {
const file =
secretFile.files[0];
if (!file) {
setStatus(
encodeStatus,
"Please select a file.",
"error"
);
return;
}
form.append(
"file",
file
);
}
setLoading(
encodeButton,
true
);
setStatus(
encodeStatus,
"Encoding..."
);
try {
const response =
await fetch(
"/api/encode",
{
method: "POST",
body: form
}
);
if (!response.ok) {
const data =
await response.json();
throw new Error(
data.error ||
"Encoding failed"
);
}
const blob =
await response.blob();
const url =
URL.createObjectURL(
blob
);
const link =
document.createElement(
"a"
);
link.href =
url;
link.download =
"encoded.png";
document.body.appendChild(
link
);
link.click();
link.remove();
URL.revokeObjectURL(
url
);
setStatus(
encodeStatus,
"Encoded successfully. Download started.",
"success"
);
} catch (error) {
setStatus(
encodeStatus,
error.message,
"error"
);
} finally {
setLoading(
encodeButton,
false
);
}
}
);
decodeForm.addEventListener(
"submit",
async (event) => {
event.preventDefault();
const image =
decodeImage.files[0];
const password =
decodePassword.value;
decodedMessage
.classList
.add("hidden");
if (!image) {
setStatus(
decodeStatus,
"Please select an encoded PNG.",
"error"
);
return;
}
const form =
new FormData();
form.append(
"image",
image
);
/*
* Password is optional.
*/
if (password) {
form.append(
"password",
password
);
}
setLoading(
decodeButton,
true
);
setStatus(
decodeStatus,
"Decoding..."
);
try {
const response =
await fetch(
"/api/decode",
{
method: "POST",
body: form
}
);
if (!response.ok) {
const data =
await response.json();
throw new Error(
data.error ||
"Decoding failed"
);
}
const contentType =
response.headers.get(
"content-type"
);
/*
* Message payload.
*/
if (
contentType &&
contentType.startsWith(
"application/json"
)
) {
const result =
await response.json();
if (
result.type !==
"message"
) {
throw new Error(
"Unknown decoded payload"
);
}
messageOutput.textContent =
result.message;
decodedMessage
.classList
.remove("hidden");
setStatus(
decodeStatus,
"Message decoded successfully.",
"success"
);
return;
}
/*
* File payload.
*/
const blob =
await response.blob();
const url =
URL.createObjectURL(
blob
);
let filename =
"extracted-file";
const disposition =
response.headers.get(
"content-disposition"
);
if (disposition) {
const match =
disposition.match(
/filename="([^"]+)"/
);
if (match) {
filename =
match[1];
}
}
const link =
document.createElement(
"a"
);
link.href =
url;
link.download =
filename;
document.body.appendChild(
link
);
link.click();
link.remove();
URL.revokeObjectURL(
url
);
setStatus(
decodeStatus,
`File extracted: ${filename}`,
"success"
);
} catch (error) {
setStatus(
decodeStatus,
error.message,
"error"
);
} finally {
setLoading(
decodeButton,
false
);
}
}
);
copyMessage.addEventListener(
"click",
async () => {
try {
await navigator.clipboard.writeText(
messageOutput.textContent
);
const original =
copyMessage.textContent;
copyMessage.textContent =
"Copied!";
setTimeout(
() => {
copyMessage.textContent =
original;
},
1500
);
} catch {
setStatus(
decodeStatus,
"Unable to copy message.",
"error"
);
}
}
);