init
This commit is contained in:
1
frontend/static/js/anime.min.js
vendored
Normal file
1
frontend/static/js/anime.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
259
frontend/static/js/app.js
Normal file
259
frontend/static/js/app.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/* 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";
|
||||
}
|
||||
|
||||
/* Fade‑out 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);
|
||||
Reference in New Issue
Block a user