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 = ` ${info.width} × ${info.height} ${info.format.toUpperCase()} · ${info.capacityKB} KB capacity `; 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" ); } } );