document.addEventListener("DOMContentLoaded", () => { const threadUrlInput = document.getElementById("thread-url"); const pasteBtn = document.getElementById("paste-btn"); const ripBtn = document.getElementById("rip-btn"); const sampleMallBtn = document.getElementById("sample-mall-btn"); const statusBanner = document.getElementById("status-banner"); const statusTitle = document.getElementById("status-title"); const statusDesc = document.getElementById("status-desc"); const resultSection = document.getElementById("result-section"); // Results elements const resAvatar = document.getElementById("res-avatar"); const resAuthorName = document.getElementById("res-author-name"); const resAuthorLink = document.getElementById("res-author-link"); const resSegmentCount = document.getElementById("res-segment-count"); const resWordCount = document.getElementById("res-word-count"); const tabSegmentCount = document.getElementById("tab-segment-count"); const readerContent = document.getElementById("reader-content"); const markdownContent = document.getElementById("markdown-content"); const segmentsContainer = document.getElementById("segments-container"); // Export buttons const copyMdBtn = document.getElementById("copy-md-btn"); const copyTxtBtn = document.getElementById("copy-txt-btn"); const downloadDropdownBtn = document.getElementById("download-dropdown-btn"); const downloadMenu = document.getElementById("download-menu"); const dlMd = document.getElementById("dl-md"); const dlTxt = document.getElementById("dl-txt"); const dlHtml = document.getElementById("dl-html"); const dlJson = document.getElementById("dl-json"); const printBtn = document.getElementById("print-btn"); // Library / History elements const toggleHistoryBtn = document.getElementById("toggle-history-btn"); const librarySidebar = document.getElementById("library-sidebar"); const closeLibraryBtn = document.getElementById("close-library-btn"); const libraryList = document.getElementById("library-list"); const libraryCount = document.getElementById("history-count"); const librarySearch = document.getElementById("library-search"); // Toast const toast = document.getElementById("toast"); let currentThread = null; let allHistory = []; let statusInterval = null; // Initialize history fetchHistory(); // Paste button pasteBtn.addEventListener("click", async () => { try { const text = await navigator.clipboard.readText(); if (text) { threadUrlInput.value = text.trim(); } } catch (e) { showToast("Clipboard access denied or unavailable", true); } }); // Example button sampleMallBtn.addEventListener("click", () => { threadUrlInput.value = sampleMallBtn.getAttribute("data-url"); triggerRip(); }); // Rip button ripBtn.addEventListener("click", () => { triggerRip(); }); threadUrlInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { triggerRip(); } }); async function triggerRip() { const url = threadUrlInput.value.trim(); if (!url) { showToast("Please paste a Threads URL", true); return; } const mode = document.querySelector('input[name="rip-mode"]:checked').value; setLoading(true); startProgressAnimation(); try { const startTime = performance.now(); const response = await fetch("/api/rip", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, mode, include_images: false, max_scrolls: 4 }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.detail || "Failed to rip thread"); } const data = await response.json(); const elapsed = ((performance.now() - startTime) / 1000).toFixed(1); currentThread = data; renderThread(data); fetchHistory(); showToast(`Thread ripped in ${elapsed}s!`); } catch (err) { console.error(err); showToast(`Error: ${err.message}`, true); } finally { stopProgressAnimation(); setLoading(false); } } function startProgressAnimation() { const steps = [ { title: "Connecting to Threads...", desc: "Bypassing heavy assets and resolving redirects..." }, { title: "Extracting continuation posts...", desc: "Tracking author replies and thread structure..." }, { title: "Stitching post segments...", desc: "Generating Markdown, HTML, and Plain Text formats..." } ]; let stepIdx = 0; statusTitle.textContent = steps[0].title; statusDesc.textContent = steps[0].desc; clearInterval(statusInterval); statusInterval = setInterval(() => { stepIdx++; if (stepIdx < steps.length) { statusTitle.textContent = steps[stepIdx].title; statusDesc.textContent = steps[stepIdx].desc; } }, 1200); } function stopProgressAnimation() { clearInterval(statusInterval); } function setLoading(isLoading) { const btnText = ripBtn.querySelector(".btn-text"); const spinner = ripBtn.querySelector(".btn-spinner"); if (isLoading) { ripBtn.disabled = true; btnText.textContent = "Ripping..."; spinner.classList.remove("hidden"); statusBanner.classList.remove("hidden"); resultSection.classList.add("hidden"); } else { ripBtn.disabled = false; btnText.textContent = "Rip Thread"; spinner.classList.add("hidden"); statusBanner.classList.add("hidden"); } } function renderThread(thread) { resultSection.classList.remove("hidden"); // Header info (clean text header, no graphic avatar) resAuthorName.textContent = thread.author_name; resAuthorLink.textContent = `@${thread.author_handle}`; resAuthorLink.href = thread.canonical_url; resAvatar.classList.add("hidden"); resSegmentCount.textContent = thread.segment_count; resWordCount.textContent = thread.total_words; tabSegmentCount.textContent = thread.segment_count; // Render Markdown view markdownContent.textContent = thread.stitched_markdown; // Render Reader view (using marked.js parser) if (window.marked) { readerContent.innerHTML = marked.parse(thread.stitched_markdown); } else { readerContent.innerHTML = thread.stitched_html; } // Render Segments breakdown view segmentsContainer.innerHTML = ""; thread.segments.forEach((seg, idx) => { const segEl = document.createElement("div"); segEl.className = "segment-item"; segEl.innerHTML = `
No threads ripped yet.
'; return; } libraryList.innerHTML = ""; items.forEach((item) => { const div = document.createElement("div"); div.className = "history-item"; div.innerHTML = `