Files
s1ne/frontend/static/js/app.js
2026-03-29 23:50:49 -05:00

260 lines
7.8 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.
/* frontend/static/js/app.js synced with backend changes */
/* jul 15 2025 */
const $ = (id) => document.getElementById(id);
const qs = (s) => encodeURIComponent(s);
const anime = window?.anime;
/* DOM references */
const urlForm = $("urlForm");
const urlInput = $("downloadURL");
const fmtCard = $("formatCard");
const platBadge = $("platformBadge");
const fmtOpt = $("formatOptions");
const hidUrl = $("hiddenUrl");
const hidSid = $("hiddenSid");
const dlBtn = $("downloadBtn");
const cancelBtn = $("cancelBtn");
const fmtTitle = $("formatTitle");
const fmtForm = $("formatForm");
const closeBtn = $("closeFormatCard");
/* Mutable state */
let fetching = false;
let downloading = false;
let esStream = null;
const sizeCache = Object.create(null);
let baseSid = sessionStorage.getItem("sid");
if (!baseSid) {
baseSid = crypto.randomUUID();
sessionStorage.setItem("sid", baseSid);
}
let sidSeq = 0;
const newSid = () => `${baseSid}-${(sidSeq++).toString(36)}`;
const hide = (el) => { el.classList.add("hidden"); el.style.display = "none"; };
const show = (el, display="block") => { el.classList.remove("hidden"); el.style.display = display; };
const errBlink = (el) => {
el.classList.add("erroring");
el.addEventListener("animationend", () => el.classList.remove("erroring"), { once:true });
};
function resetBtn() {
dlBtn.disabled = true;
dlBtn.classList.remove("loading");
dlBtn.textContent = "Download";
}
/* Fadeout animation for format card */
function fadeOutCard() {
if (fmtCard.classList.contains("hidden")) return;
anime({
targets: fmtCard,
opacity: [1,0],
translateY: [0,-10],
easing: "easeInQuad",
duration: 300,
complete: () => {
hide(fmtCard);
fmtOpt.innerHTML = "";
}
});
platBadge.style.display = "none";
}
function resetUI() {
downloading = false;
fadeOutCard();
resetBtn();
urlInput.value = "";
urlInput.disabled = false;
urlInput.classList.remove("fetching");
urlInput.focus();
}
/* SSE helpers */
function closeStream() { esStream?.close(); esStream = null; }
function startSSE(sid, onFinish = () => {}) {
closeStream();
esStream = new EventSource(`/api/progress/${sid}`);
esStream.onmessage = ({ data }) => {
const j = JSON.parse(data);
if (["finished","error","cancelled","cached"].includes(j.status)) {
finish();
onFinish();
}
};
esStream.onerror = closeStream;
}
/* Finalise flow (download finished / errored / cancelled) */
function finish() {
closeStream();
resetUI();
}
/* Cancel current download or dismiss card */
function cancelFlow() {
if (downloading) {
fetch(`/cancel_download?sid=${hidSid.value}`, { method:"POST" })
.finally(finish);
} else {
fadeOutCard();
}
}
urlInput.addEventListener("keydown", (e) => {
if (e.key !== "Enter") return;
e.preventDefault();
if (!fetching && !downloading) urlForm.requestSubmit();
});
urlForm.addEventListener("submit", async (e) => {
e.preventDefault();
if (fetching || downloading) return;
const raw = urlInput.value.trim();
if (!raw) { errBlink(urlInput); return; }
fetching = true;
urlInput.classList.add("fetching");
urlInput.disabled = true;
const maxRetries = 3;
let lastError = null;
let result = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const r = await fetch("/choose_format", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: raw })
});
result = await r.json().catch(() => ({ error: "invalid json" }));
if (r.ok && result && !result.error && Array.isArray(result.formats)) {
break;
}
lastError = result?.error || "Unknown error";
} catch (err) {
lastError = "Network error";
}
if (attempt < maxRetries) {
await new Promise(res => setTimeout(res, 750 * attempt));
}
}
if (!result || result.error || !Array.isArray(result.formats)) {
errBlink(urlInput);
console.warn("choose_format failed:", lastError);
fetching = false;
urlInput.classList.remove("fetching");
urlInput.disabled = false;
return;
}
if (!result.sid) {
errBlink(urlInput);
console.error("Missing sid from server response");
fetching = false;
urlInput.classList.remove("fetching");
urlInput.disabled = false;
return;
}
const sid = result.sid;
hidSid.value = sid; // ✅ Use server-issued SID only
hidUrl.value = result.url;
fmtTitle.textContent = result.title || "Select format";
const platform = (result.platform || "other").toLowerCase();
platBadge.textContent = platform === "twitterx" ? "X / Twitter" :
platform[0].toUpperCase() + platform.slice(1);
platBadge.className = `radix-badge badge-${platform}`;
show(platBadge, "inline-flex");
platBadge.style.opacity = 0;
anime({ targets: platBadge, translateY: [-10, 0], opacity: [0, 1], duration: 300 });
fmtOpt.innerHTML = "";
const sourceUrl = result.url;
result.formats.forEach((f) => {
const row = document.createElement("div");
row.className = "format-option";
const radio = document.createElement("input");
radio.type = "radio";
radio.id = `f_${f.format_id}`;
radio.name = "format_id";
radio.value = f.format_id;
const label = document.createElement("label");
label.htmlFor = radio.id;
label.textContent = f.label;
row.append(radio, label);
// Size prefetch
let hoverTimer = null;
const fetchSize = async () => {
if (sizeCache[f.format_id] || downloading) return;
try {
const szResp = await fetch(`/format_size?url=${qs(sourceUrl)}&fmt_id=${f.format_id}`);
const js = await szResp.json();
if (szResp.ok && js.size) {
const mb = js.size / 1_048_576;
sizeCache[f.format_id] = `${(mb > 99 ? Math.round(mb) : mb.toFixed(1))}\u00A0MB`;
label.dataset.size = sizeCache[f.format_id];
}
} catch {}
};
row.addEventListener("mouseenter", () => hoverTimer = setTimeout(fetchSize, 1200));
row.addEventListener("mouseleave", () => clearTimeout(hoverTimer));
radio.addEventListener("change", () => { dlBtn.disabled = false; });
fmtOpt.append(row);
});
show(fmtCard);
fmtCard.style.opacity = 0;
anime({ targets: fmtCard, opacity: [0, 1], translateY: [-10, 0], duration: 300 });
fetching = false;
urlInput.classList.remove("fetching");
});
fmtForm.addEventListener("submit", (e) => {
e.preventDefault();
if (downloading) return;
const fmtId = new FormData(fmtForm).get("format_id");
if (!fmtId) { errBlink(dlBtn); return; }
downloading = true;
const sid = hidSid.value;
dlBtn.disabled = true;
dlBtn.classList.add("loading");
dlBtn.textContent = "Downloading";
startSSE(sid);
const ifr = document.createElement("iframe");
ifr.style.display = "none";
ifr.src = `/download_file?sid=${sid}&url=${qs(hidUrl.value)}&format_id=${fmtId}`;
document.body.appendChild(ifr);
/* fadeoutcard not needed */
fadeOutCard();
});
cancelBtn.addEventListener("click", cancelFlow);
closeBtn .addEventListener("click", cancelFlow);