(() => {
const t = (k, vars) => window.WrappedI18n.t(k, vars);
/** @type {File[]} */
const files = [];
let settings = null;
let captchaWidgetId = null;
const els = {
text: document.getElementById("payload-text"),
lang: document.getElementById("text-language"),
langChip: document.getElementById("lang-chip"),
langChipText: document.getElementById("lang-chip-text"),
langModal: document.getElementById("lang-modal"),
langGrid: document.getElementById("lang-grid"),
highlight: document.getElementById("code-highlight"),
editor: document.getElementById("code-editor"),
ttl: document.getElementById("ttl-seconds"),
maxOpens: document.getElementById("max-opens"),
availableFromDate: document.getElementById("available-from-date"),
availableFromTime: document.getElementById("available-from-time"),
availableFromDateBtn: document.getElementById("available-from-date-btn"),
availableFromTimeBtn: document.getElementById("available-from-time-btn"),
password: document.getElementById("password"),
generatePassword: document.getElementById("generate-password"),
dropzone: document.getElementById("dropzone"),
fileInput: document.getElementById("file-input"),
fileList: document.getElementById("file-list"),
wrapBtn: document.getElementById("wrap-btn"),
error: document.getElementById("form-error"),
success: document.getElementById("success-panel"),
composer: document.querySelector(".composer"),
shareLink: document.getElementById("share-link"),
shareToken: document.getElementById("share-token"),
sharePassword: document.getElementById("share-password"),
passwordBlock: document.getElementById("success-password-block"),
successMetaBadges: document.getElementById("success-meta-badges"),
shareQr: document.getElementById("share-qr"),
expiresMeta: document.getElementById("expires-meta"),
captchaSlot: document.getElementById("captcha-slot"),
sizeMeter: document.getElementById("size-meter"),
shareNative: document.getElementById("share-native"),
downloadQr: document.getElementById("download-qr"),
};
function showError(msg) {
els.error.textContent = msg;
els.error.classList.remove("hidden");
}
function clearError() {
els.error.classList.add("hidden");
els.error.textContent = "";
}
function mimeAllowed(ct) {
const allow = settings.mime_allowlist || [];
const base = (ct || "").split(";")[0].trim().toLowerCase();
return allow.some((p) => {
const pat = p.toLowerCase();
if (pat.endsWith("/*")) return base.startsWith(pat.slice(0, -1));
return base === pat;
});
}
function langLabel(id) {
return t(`lang.${id}`) || id;
}
function setLanguage(id, { silent = false } = {}) {
const lang = window.WrappedHighlight.languages().includes(id) ? id : "plaintext";
els.lang.value = lang;
if (els.langChipText) els.langChipText.textContent = langLabel(lang);
if (!silent) refreshHighlight();
}
function refreshHighlight() {
window.WrappedHighlight.highlightElement(
els.highlight,
els.text.value,
els.lang.value || "plaintext"
);
syncEditorHeight();
}
function syncEditorHeight() {
if (!els.text || !els.highlight) return;
els.highlight.parentElement.style.height = "auto";
const pre = els.highlight.parentElement;
pre.style.minHeight = `${Math.max(220, els.text.scrollHeight)}px`;
}
function openLangModal() {
if (!els.langModal || !els.langGrid) return;
els.langGrid.innerHTML = "";
for (const id of window.WrappedHighlight.languages()) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "lang-option" + (id === els.lang.value ? " active" : "");
btn.textContent = langLabel(id);
btn.addEventListener("click", () => {
setLanguage(id);
closeLangModal();
});
els.langGrid.appendChild(btn);
}
els.langModal.classList.remove("hidden");
document.body.classList.add("modal-open");
}
function closeLangModal() {
if (!els.langModal) return;
els.langModal.classList.add("hidden");
document.body.classList.remove("modal-open");
}
function estimatePayloadBytes() {
const textBytes = new TextEncoder().encode(els.text?.value || "").length;
const fileBytes = files.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
return textBytes + fileBytes;
}
function refreshSizeMeter() {
if (!els.sizeMeter || !settings) return;
const used = estimatePayloadBytes();
const max = Number(settings.max_upload_bytes) || 0;
if (!max) {
els.sizeMeter.textContent = "";
els.sizeMeter.classList.remove("is-warn", "is-over");
return;
}
const fmt = window.WrappedUI.formatBytes;
let text = t("create.sizeMeter", {
used: fmt(used),
max: fmt(max),
});
const near = used > max * 0.85 && used <= max;
const over = used > max;
if (near) text = `${text} · ${t("create.sizeNearLimit")}`;
els.sizeMeter.textContent = text;
els.sizeMeter.classList.toggle("is-over", over);
els.sizeMeter.classList.toggle("is-warn", near);
}
function renderFiles() {
els.fileList.innerHTML = "";
files.forEach((f, idx) => {
const li = document.createElement("li");
li.innerHTML = `${escapeHtml(f.name)} (${escapeHtml(f.type || "file")} · ${window.WrappedUI.formatBytes(f.size)})`;
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = "×";
btn.addEventListener("click", () => {
files.splice(idx, 1);
renderFiles();
});
li.appendChild(btn);
els.fileList.appendChild(li);
});
refreshSizeMeter();
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function addFiles(list) {
for (const f of list) {
if (!mimeAllowed(f.type || "application/octet-stream")) {
showError(t("create.mimeDenied") + `: ${f.type || f.name}`);
continue;
}
files.push(f);
}
renderFiles();
}
let lastExpiresAt = null;
let lastAvailableFrom = null;
let lastMaxOpens = 1;
function secondsUntilEvening() {
const now = new Date();
const target = new Date(now);
target.setHours(20, 0, 0, 0);
if (target <= now) target.setDate(target.getDate() + 1);
return Math.max(60, Math.round((target.getTime() - now.getTime()) / 1000));
}
function fillTtl() {
if (!settings || !els.ttl) return;
const max = settings.max_ttl_seconds;
const def = settings.default_ttl_seconds;
const selected = els.ttl.value ? Number(els.ttl.value) : def;
const evening = secondsUntilEvening();
const options = [
{ key: "create.ttl.1h", value: 3600 },
{ key: "create.ttl.evening", value: evening },
{ key: "create.ttl.24h", value: 24 * 3600 },
{ key: "create.ttl.3d", value: 3 * 24 * 3600 },
{ key: "create.ttl.7d", value: 7 * 24 * 3600 },
].filter((o) => o.value <= max);
if (!options.find((o) => o.value === def) && def <= max) {
options.unshift({ key: "create.ttl.default", value: def, seconds: def });
}
const pick = options.some((o) => o.value === selected) ? selected : def;
els.ttl.innerHTML = options
.map((o) => {
const label =
o.key === "create.ttl.default"
? t("create.ttl.default", { seconds: o.seconds })
: t(o.key);
return ``;
})
.join("");
}
function fillMaxOpens() {
if (!els.maxOpens || !settings) return;
const limit = Math.max(1, Math.min(10, Number(settings.max_opens_limit) || 3));
const uiMax = Math.min(3, limit);
const selected = els.maxOpens.value ? Number(els.maxOpens.value) : 1;
const pick = selected >= 1 && selected <= uiMax ? selected : 1;
els.maxOpens.innerHTML = Array.from({ length: uiMax }, (_, i) => i + 1)
.map(
(n) =>
``
)
.join("");
}
function availableFromToIso() {
const date = (els.availableFromDate?.value || "").trim();
if (!date) return null;
const time = (els.availableFromTime?.value || "").trim() || "00:00";
const d = new Date(`${date}T${time}`);
if (Number.isNaN(d.getTime())) return null;
return d.toISOString();
}
function formatLocalDateTime(iso) {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return String(iso);
return new Intl.DateTimeFormat(window.WrappedI18n.locale?.() || undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(d);
}
function refreshSuccessBadges() {
if (!els.successMetaBadges) return;
els.successMetaBadges.innerHTML = "";
if (lastMaxOpens > 1) {
const b = document.createElement("span");
b.className = "success-meta-badge";
b.textContent = t("create.opensBadge", { n: lastMaxOpens });
els.successMetaBadges.appendChild(b);
}
if (lastAvailableFrom) {
const b = document.createElement("span");
b.className = "success-meta-badge";
b.textContent = t("create.availableFromBadge", {
datetime: formatLocalDateTime(lastAvailableFrom),
});
els.successMetaBadges.appendChild(b);
}
}
function refreshExpiresMeta() {
if (lastExpiresAt && els.expiresMeta) {
els.expiresMeta.innerHTML = window.WrappedI18n.formatExpiresHtml
? window.WrappedI18n.formatExpiresHtml(lastExpiresAt)
: window.WrappedI18n.formatExpires(lastExpiresAt);
}
}
function loadCaptcha() {
els.captchaSlot.innerHTML = "";
captchaWidgetId = null;
const provider = settings.captcha_provider;
if (provider === "turnstile" && settings.turnstile_site_key) {
const div = document.createElement("div");
div.className = "cf-turnstile";
div.dataset.sitekey = settings.turnstile_site_key;
els.captchaSlot.appendChild(div);
const s = document.createElement("script");
s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
s.async = true;
document.body.appendChild(s);
} else if (provider === "hcaptcha" && settings.hcaptcha_site_key) {
const div = document.createElement("div");
div.className = "h-captcha";
div.dataset.sitekey = settings.hcaptcha_site_key;
els.captchaSlot.appendChild(div);
const s = document.createElement("script");
s.src = "https://js.hcaptcha.com/1/api.js";
s.async = true;
document.body.appendChild(s);
}
}
function captchaToken() {
if (settings.captcha_provider === "turnstile" && window.turnstile) {
return window.turnstile.getResponse?.() || "";
}
if (settings.captcha_provider === "hcaptcha" && window.hcaptcha) {
return window.hcaptcha.getResponse?.(captchaWidgetId) || window.hcaptcha.getResponse?.() || "";
}
return "";
}
async function init() {
const resp = await fetch("/api/v1/settings");
settings = await resp.json();
fillTtl();
fillMaxOpens();
loadCaptcha();
setLanguage("plaintext", { silent: true });
refreshHighlight();
refreshSizeMeter();
syncShareControls();
}
els.dropzone.addEventListener("click", () => els.fileInput.click());
els.fileInput.addEventListener("change", () => addFiles(els.fileInput.files));
["dragenter", "dragover"].forEach((ev) => {
els.dropzone.addEventListener(ev, (e) => {
e.preventDefault();
els.dropzone.classList.add("dragover");
});
});
["dragleave", "drop"].forEach((ev) => {
els.dropzone.addEventListener(ev, (e) => {
e.preventDefault();
els.dropzone.classList.remove("dragover");
});
});
els.dropzone.addEventListener("drop", (e) => addFiles(e.dataTransfer.files));
document.addEventListener("paste", (e) => {
const items = e.clipboardData?.items;
if (!items) return;
const pasted = [];
for (const item of items) {
if (item.kind === "file") {
const f = item.getAsFile();
if (f) pasted.push(f);
}
}
if (pasted.length) {
e.preventDefault();
addFiles(pasted);
}
});
els.text.addEventListener("input", () => {
refreshHighlight();
refreshSizeMeter();
});
els.text.addEventListener("scroll", () => {
const pre = els.highlight.parentElement;
pre.scrollTop = els.text.scrollTop;
pre.scrollLeft = els.text.scrollLeft;
});
els.langChip?.addEventListener("click", openLangModal);
els.langModal?.addEventListener("click", (e) => {
if (e.target.closest("[data-lang-close]")) closeLangModal();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && els.langModal && !els.langModal.classList.contains("hidden")) {
closeLangModal();
}
});
function renderShareQr(link) {
if (!els.shareQr) return;
els.shareQr.innerHTML = "";
if (!link || typeof QRCode === "undefined") return;
const size = window.matchMedia("(max-width: 860px)").matches ? 200 : 164;
// eslint-disable-next-line no-new
new QRCode(els.shareQr, {
text: link,
width: size,
height: size,
colorDark: "#0d1420",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M,
});
}
function syncShareControls() {
if (els.shareNative) {
const canShare = typeof navigator.share === "function";
els.shareNative.classList.toggle("hidden", !canShare);
els.shareNative.setAttribute("aria-label", t("create.share"));
}
if (els.downloadQr) {
els.downloadQr.setAttribute("aria-label", t("create.downloadQr"));
}
}
function showSuccess({ link, token, password, expiresAt, availableFrom, maxOpens }) {
els.shareLink.value = link;
els.shareToken.value = token;
lastExpiresAt = expiresAt;
lastAvailableFrom = availableFrom || null;
lastMaxOpens = maxOpens || 1;
refreshExpiresMeta();
refreshSuccessBadges();
if (password) {
els.sharePassword.value = password;
els.passwordBlock.classList.remove("hidden");
} else {
els.sharePassword.value = "";
els.passwordBlock.classList.add("hidden");
}
renderShareQr(link);
syncShareControls();
els.composer.classList.add("hidden");
els.success.classList.remove("hidden");
els.success.scrollIntoView({ behavior: "smooth", block: "start" });
}
async function copyFrom(input, btn) {
try {
await window.WrappedUI.copyText(input.value);
} catch {
await navigator.clipboard.writeText(input.value);
}
const old = btn.textContent;
btn.textContent = t("common.copied");
setTimeout(() => (btn.textContent = old), 1200);
}
function downloadQrPng() {
if (!els.shareQr) return;
const canvas = els.shareQr.querySelector("canvas");
const img = els.shareQr.querySelector("img");
let href = "";
if (canvas && canvas.toDataURL) {
href = canvas.toDataURL("image/png");
} else if (img?.src) {
href = img.src;
}
if (!href) return;
const a = document.createElement("a");
a.href = href;
a.download = "wrapped-qr.png";
a.click();
}
document.getElementById("copy-link").addEventListener("click", () => {
copyFrom(els.shareLink, document.getElementById("copy-link"));
});
document.getElementById("copy-token").addEventListener("click", () => {
copyFrom(els.shareToken, document.getElementById("copy-token"));
});
document.getElementById("copy-password")?.addEventListener("click", () => {
copyFrom(els.sharePassword, document.getElementById("copy-password"));
});
els.shareNative?.addEventListener("click", async () => {
if (typeof navigator.share !== "function" || !els.shareLink.value) return;
try {
await navigator.share({
title: t("create.shareTitle"),
text: t("create.shareText"),
url: els.shareLink.value,
});
} catch {
/* user cancelled or unsupported */
}
});
els.downloadQr?.addEventListener("click", downloadQrPng);
document.getElementById("create-another").addEventListener("click", () => {
location.reload();
});
els.wrapBtn.addEventListener("click", async () => {
clearError();
const text = els.text.value;
if (!text.trim() && files.length === 0) {
showError(t("create.needPayload"));
return;
}
const items = [];
const contentTypes = [];
if (text.trim()) {
items.push({
type: "text",
language: els.lang.value,
content: text,
});
contentTypes.push("text/plain");
}
for (const f of files) {
const item = await window.WrappedCrypto.fileToItem(f);
items.push(item);
contentTypes.push(item.mime);
}
for (const ct of contentTypes) {
if (!mimeAllowed(ct)) {
showError(t("create.mimeDenied"));
return;
}
}
const password = els.password.value || "";
const maxOpens = Number(els.maxOpens?.value || 1);
const availableFromIso = availableFromToIso();
els.wrapBtn.disabled = true;
window.WrappedUI.showBusy(t("create.working"));
try {
const pack = { version: 1, items };
const { ciphertext, keyB64url } = await window.WrappedCrypto.encryptPackage(
pack,
password || null
);
if (ciphertext.byteLength > settings.max_upload_bytes) {
showError(t("create.tooLarge"));
return;
}
const body = {
ciphertext_b64: window.WrappedCrypto.bytesToBase64(ciphertext),
ttl_seconds: Number(els.ttl.value),
content_types: contentTypes,
item_count: items.length,
has_password: Boolean(password),
password: password || null,
captcha_token: captchaToken() || null,
max_opens: maxOpens,
available_from: availableFromIso,
};
const resp = await fetch("/api/v1/wraps", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
const detail = err.detail;
if (detail === "available_from_past") {
showError(t("create.availableFromPast"));
return;
}
if (detail === "available_from_after_expiry") {
showError(t("create.availableFromAfterExpiry"));
return;
}
if (detail === "max_opens_invalid") {
showError(t("create.maxOpensInvalid"));
return;
}
showError(typeof detail === "string" ? detail : t("common.error"));
return;
}
const data = await resp.json();
const token = window.WrappedCrypto.buildToken(data.wrap_id, keyB64url);
const link = `${location.origin}${data.share_path}#${keyB64url}`;
showSuccess({
link,
token,
password,
expiresAt: data.expires_at,
availableFrom: data.available_from,
maxOpens: data.max_opens || maxOpens,
});
} catch {
showError(t("common.error"));
} finally {
window.WrappedUI.hideBusy();
els.wrapBtn.disabled = false;
}
});
function generatePassword(length = 20) {
const alphabet =
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*-_";
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
}
els.generatePassword?.addEventListener("click", () => {
const pwd = generatePassword();
els.password.type = "text";
els.password.value = pwd;
els.password.focus();
els.password.select();
});
function openPicker(input) {
if (!input) return;
try {
if (typeof input.showPicker === "function") {
input.showPicker();
return;
}
} catch {
/* ignore */
}
input.focus();
input.click();
}
els.availableFromDateBtn?.addEventListener("click", () => openPicker(els.availableFromDate));
els.availableFromTimeBtn?.addEventListener("click", () => openPicker(els.availableFromTime));
if (window.WrappedI18n.onChange) {
window.WrappedI18n.onChange(() => {
fillTtl();
fillMaxOpens();
refreshExpiresMeta();
refreshSuccessBadges();
refreshSizeMeter();
syncShareControls();
setLanguage(els.lang.value, { silent: true });
refreshHighlight();
if (els.langChipText) els.langChipText.textContent = langLabel(els.lang.value);
});
}
init().catch(() => showError(t("common.error")));
})();