// Feed / reader mode. Presents the stories currently on the page one at a // time in a focused overlay, marking each read as it's shown. Left/right arrow // keys (or the on-screen buttons) page through them; the full article text is // whatever Pete captured at ingest (content:encoded or the scraped body), // fetched on demand from /api/article and cached for the session. // // Read state lives in localStorage under pete.read.v1. It's deliberately // device-local — unlike the small prefs blob, the read set grows unbounded, so // it isn't mirrored through PetePrefs to the account. (function () { var overlay = document.getElementById("pete-reader"); if (!overlay) return; var READ_KEY = "pete.read.v1"; var articleEl = overlay.querySelector("[data-reader-article]"); var scrollEl = overlay.querySelector("[data-reader-scroll]"); var progressEl = overlay.querySelector("[data-reader-progress]"); var linkEl = overlay.querySelector("[data-reader-link]"); var prevBtn = overlay.querySelector("[data-reader-prev]"); var nextBtn = overlay.querySelector("[data-reader-next]"); var closeBtn = overlay.querySelector("[data-reader-close]"); var backdrop = overlay.querySelector("[data-reader-backdrop]"); var items = []; // {id, url, headline, lede, image, time, source, chTitle, chEmoji, chTheme, posted, paywalled} var index = 0; var open = false; var contentCache = {}; // id -> {content, lede} var inflight = null; // ---- read-state store ----------------------------------------------------- function loadRead() { try { var raw = localStorage.getItem(READ_KEY); var obj = raw ? JSON.parse(raw) : {}; return obj && typeof obj === "object" ? obj : {}; } catch (e) { return {}; } } function saveRead(set) { try { localStorage.setItem(READ_KEY, JSON.stringify(set)); } catch (e) {} } var readSet = loadRead(); function isRead(id) { return !!readSet[id]; } function setRead(id, on) { if (on) readSet[id] = 1; else delete readSet[id]; saveRead(readSet); paintCard(id, on); } // Reflect read state onto every matching card on the page (a story can appear // in more than one section on the home page). function paintCard(id, on) { document.querySelectorAll('[data-story-card][data-id="' + cssEsc(id) + '"]').forEach(function (c) { if (on) c.setAttribute("data-read", "1"); else c.removeAttribute("data-read"); }); } function cssEsc(s) { return String(s).replace(/["\\]/g, "\\$&"); } // ---- collecting the page's stories ---------------------------------------- function isVisible(el) { return window.getComputedStyle(el).display !== "none"; } function collect() { var seen = Object.create(null); var out = []; document.querySelectorAll("[data-story-card]").forEach(function (c) { var id = c.getAttribute("data-id"); if (!id || seen[id] || !isVisible(c)) return; var url = c.getAttribute("data-url") || c.getAttribute("href") || ""; if (!/^https?:\/\//i.test(url)) url = ""; seen[id] = true; out.push({ id: id, url: url, headline: c.getAttribute("data-headline") || "", lede: c.getAttribute("data-lede") || "", image: c.getAttribute("data-image") || "", time: c.getAttribute("data-time") || "", source: c.getAttribute("data-source") || "", chTitle: c.getAttribute("data-ch-title") || "", chEmoji: c.getAttribute("data-ch-emoji") || "", chTheme: c.getAttribute("data-ch-theme") || "", paywalled: c.getAttribute("data-paywalled") === "1" }); }); return out; } // ---- rendering ------------------------------------------------------------ function escapeHTML(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); } function safeURL(s) { var raw = String(s == null ? "" : s).trim(); return /^https?:\/\//i.test(raw) ? raw : ""; } // Turn stored plain text (paragraphs separated by blank lines) into escaped //
blocks. Single newlines inside a paragraph become spaces. function paragraphs(text) { var blocks = String(text || "").split(/\n{2,}/); var html = ""; for (var i = 0; i < blocks.length; i++) { var t = blocks[i].replace(/\s*\n\s*/g, " ").trim(); if (t) html += "
" + escapeHTML(t) + "
"; } return html; } function eyebrow(it) { var chip = it.chTheme ? '' + (it.chEmoji ? '" : "") + escapeHTML(it.chTitle) + "" : ""; var src = it.source ? '' + escapeHTML(it.source) + "" : ""; var time = it.time ? '' + escapeHTML(it.time) + "" : ""; return '' + (it.paywalled ? "This source is paywalled — the text above may be partial. " : "") + (href ? 'Read it at the source: ' + escapeHTML(it.source || "original article") + " ↗." : "") + "
"; articleEl.innerHTML = eyebrow(it) + 'Loading the full story…
'); } function fillContent(it, data) { var text = (data && data.content) ? data.content : ((data && data.lede) || it.lede); var html = paragraphs(text); if (!html) { html = 'No article text was captured for this story. Open the original to read it.
'; } renderBody(it, html); } function fetchContent(it) { if (contentCache[it.id]) { fillContent(it, contentCache[it.id]); return; } loadingBody(it); if (inflight) inflight.abort(); var ctrl = new AbortController(); inflight = ctrl; var reqId = it.id; fetch("/api/article?id=" + encodeURIComponent(it.id), { signal: ctrl.signal, credentials: "same-origin" }) .then(function (r) { if (!r.ok) throw new Error("status " + r.status); return r.json(); }) .then(function (data) { contentCache[reqId] = data; if (ctrl !== inflight) return; // superseded by a newer navigation if (items[index] && items[index].id === reqId) fillContent(it, data); }) .catch(function (err) { if (err.name === "AbortError") return; if (items[index] && items[index].id === reqId) fillContent(it, null); // fall back to lede }) .finally(function () { if (ctrl === inflight) inflight = null; }); } // ---- navigation ----------------------------------------------------------- function show(i) { index = i; if (index >= items.length) { renderDone(); return; } var it = items[index]; progressEl.textContent = (index + 1) + " / " + items.length; var href = safeURL(it.url); if (href) { linkEl.href = href; linkEl.style.display = ""; } else linkEl.style.display = "none"; prevBtn.disabled = index === 0; nextBtn.disabled = false; nextBtn.textContent = index === items.length - 1 ? "done ✓" : "→"; if (scrollEl) scrollEl.scrollTop = 0; fetchContent(it); setRead(it.id, true); // presenting a story marks it read } function renderDone() { progressEl.textContent = items.length + " / " + items.length; linkEl.style.display = "none"; prevBtn.disabled = items.length === 0; nextBtn.disabled = true; nextBtn.textContent = "→"; if (scrollEl) scrollEl.scrollTop = 0; articleEl.innerHTML = 'That\'s every story on this page. ' + "Head back to browse more, or press ← to revisit the last one.
" + "