330 lines
10 KiB
JavaScript
330 lines
10 KiB
JavaScript
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 = [];
|
|
|
|
// 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);
|
|
statusTitle.textContent = "Connecting to Threads...";
|
|
statusDesc.textContent = "Launching headless browser and resolving URL...";
|
|
|
|
try {
|
|
const response = await fetch("/api/rip", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ url, mode, max_scrolls: 5 })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json();
|
|
throw new Error(err.detail || "Failed to rip thread");
|
|
}
|
|
|
|
const data = await response.json();
|
|
currentThread = data;
|
|
renderThread(data);
|
|
fetchHistory();
|
|
showToast("Thread successfully ripped & saved!");
|
|
} catch (err) {
|
|
console.error(err);
|
|
showToast(`Error: ${err.message}`, true);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
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
|
|
resAuthorName.textContent = thread.author_name;
|
|
resAuthorLink.textContent = `@${thread.author_handle}`;
|
|
resAuthorLink.href = thread.canonical_url;
|
|
if (thread.author_avatar) {
|
|
resAvatar.src = thread.author_avatar;
|
|
resAvatar.classList.remove("hidden");
|
|
} else {
|
|
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 = `
|
|
<div class="segment-header">
|
|
<span class="segment-num">Segment #${idx + 1} ${seg.is_author ? "(Author)" : "(Reply)"}</span>
|
|
<span>${seg.author_handle ? "@" + seg.author_handle : ""}</span>
|
|
</div>
|
|
<div class="segment-text">${escapeHtml(seg.text)}</div>
|
|
`;
|
|
segmentsContainer.appendChild(segEl);
|
|
});
|
|
|
|
// Setup download links
|
|
dlMd.href = `/api/download/${thread.id}/md`;
|
|
dlTxt.href = `/api/download/${thread.id}/txt`;
|
|
dlHtml.href = `/api/download/${thread.id}/html`;
|
|
dlJson.href = `/api/download/${thread.id}/json`;
|
|
|
|
// Switch to reader view
|
|
switchTab("reader");
|
|
}
|
|
|
|
// Tabs switching
|
|
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const tabName = btn.getAttribute("data-tab");
|
|
switchTab(tabName);
|
|
});
|
|
});
|
|
|
|
function switchTab(tabName) {
|
|
document.querySelectorAll(".tab-btn").forEach((b) => b.classList.remove("active"));
|
|
document.querySelector(`.tab-btn[data-tab="${tabName}"]`)?.classList.add("active");
|
|
|
|
document.querySelectorAll(".tab-view").forEach((v) => {
|
|
v.classList.add("hidden");
|
|
v.classList.remove("active");
|
|
});
|
|
|
|
const targetView = document.getElementById(`view-${tabName}`);
|
|
if (targetView) {
|
|
targetView.classList.remove("hidden");
|
|
targetView.classList.add("active");
|
|
}
|
|
}
|
|
|
|
// Copy buttons
|
|
copyMdBtn.addEventListener("click", () => {
|
|
if (!currentThread) return;
|
|
navigator.clipboard.writeText(currentThread.stitched_markdown);
|
|
showToast("Copied Markdown to clipboard!");
|
|
});
|
|
|
|
copyTxtBtn.addEventListener("click", () => {
|
|
if (!currentThread) return;
|
|
navigator.clipboard.writeText(currentThread.stitched_text);
|
|
showToast("Copied Plain Text to clipboard!");
|
|
});
|
|
|
|
// Download dropdown toggle
|
|
downloadDropdownBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
downloadMenu.classList.toggle("show");
|
|
});
|
|
|
|
document.addEventListener("click", () => {
|
|
downloadMenu.classList.remove("show");
|
|
});
|
|
|
|
// Print button
|
|
printBtn.addEventListener("click", () => {
|
|
if (!currentThread) return;
|
|
const printWindow = window.open("", "_blank");
|
|
printWindow.document.write(currentThread.stitched_html);
|
|
printWindow.document.close();
|
|
printWindow.focus();
|
|
setTimeout(() => {
|
|
printWindow.print();
|
|
}, 500);
|
|
});
|
|
|
|
// Library / History
|
|
toggleHistoryBtn.addEventListener("click", () => {
|
|
librarySidebar.classList.toggle("open");
|
|
});
|
|
|
|
closeLibraryBtn.addEventListener("click", () => {
|
|
librarySidebar.classList.remove("open");
|
|
});
|
|
|
|
async function fetchHistory() {
|
|
try {
|
|
const res = await fetch("/api/history");
|
|
if (res.ok) {
|
|
allHistory = await res.json();
|
|
libraryCount.textContent = allHistory.length;
|
|
renderHistoryList(allHistory);
|
|
}
|
|
} catch (e) {
|
|
console.warn("Could not load history", e);
|
|
}
|
|
}
|
|
|
|
function renderHistoryList(items) {
|
|
if (!items || items.length === 0) {
|
|
libraryList.innerHTML = '<p class="empty-state">No threads ripped yet.</p>';
|
|
return;
|
|
}
|
|
|
|
libraryList.innerHTML = "";
|
|
items.forEach((item) => {
|
|
const div = document.createElement("div");
|
|
div.className = "history-item";
|
|
div.innerHTML = `
|
|
<div class="history-item-title">${escapeHtml(item.title)}</div>
|
|
<div class="history-item-meta">
|
|
<span>@${escapeHtml(item.author_handle)} (${item.segment_count} posts)</span>
|
|
<span>${item.saved_at.split("T")[0] || ""}</span>
|
|
</div>
|
|
`;
|
|
div.addEventListener("click", async () => {
|
|
try {
|
|
const res = await fetch(`/api/history/${item.id}`);
|
|
if (res.ok) {
|
|
currentThread = await res.json();
|
|
renderThread(currentThread);
|
|
librarySidebar.classList.remove("open");
|
|
}
|
|
} catch (e) {
|
|
showToast("Failed to load thread", true);
|
|
}
|
|
});
|
|
libraryList.appendChild(div);
|
|
});
|
|
}
|
|
|
|
librarySearch.addEventListener("input", (e) => {
|
|
const q = e.target.value.toLowerCase();
|
|
const filtered = allHistory.filter(
|
|
(item) =>
|
|
item.title.toLowerCase().includes(q) ||
|
|
item.author_handle.toLowerCase().includes(q)
|
|
);
|
|
renderHistoryList(filtered);
|
|
});
|
|
|
|
function showToast(msg, isError = false) {
|
|
toast.textContent = msg;
|
|
toast.style.background = isError ? "#da3633" : "#238636";
|
|
toast.classList.remove("hidden");
|
|
setTimeout(() => {
|
|
toast.classList.add("hidden");
|
|
}, 3500);
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return "";
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
});
|