first commit
This commit is contained in:
+936
@@ -0,0 +1,936 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user