Add feed/reader mode with full-article capture at ingest
Reader mode presents the stories on a page one at a time in a focused overlay, marking each read as it's shown. Left/right arrows (or the header book button / `f`) page through them; read stories dim on the grid. Read state is device-local in localStorage. Backing this required actually capturing article bodies, which Pete wasn't doing — it kept only the RSS <description> lede and discarded content:encoded: - stories.content column (idempotent migration; old rows fall back to lede) - parser keeps content:encoded as paragraph-preserving text - article fetch already done for paywall detection now also returns its body, so ingest stores the richer of feed-content vs scraped body with no extra request (prefers the archive snapshot body for paywalled stories) - GET /api/article?id= serves the stored text; card queries now select id and expose it as data-id for the reader Tests cover content extraction, the storage round-trip, and the article endpoint + card rendering end to end.
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
{{define "card"}}
|
||||
{{define "card"}}{{$ch := channel .Story.Channel}}
|
||||
<a href="{{.Story.ArticleURL}}" target="_blank" rel="noopener noreferrer"
|
||||
data-story-card data-source="{{.Story.Source}}" data-channel="{{.Story.Channel}}"
|
||||
data-id="{{.Story.ID}}"
|
||||
data-headline="{{.Story.Headline}}"
|
||||
data-lede="{{.Story.Lede}}"
|
||||
data-url="{{.Story.ArticleURL}}"
|
||||
data-image="{{if .Story.ImageURL}}{{thumb .Story.ImageURL}}{{end}}"
|
||||
data-time="{{timeAgo .Story.SeenAt}}"
|
||||
data-ch-title="{{$ch.Title}}" data-ch-emoji="{{$ch.Emoji}}" data-ch-theme="{{$ch.Theme}}"
|
||||
data-posted="{{if .Story.Posted}}1{{end}}"
|
||||
data-paywalled="{{if .Story.Paywalled}}1{{end}}"
|
||||
class="group relative block rounded-3xl bg-[color:var(--card)] border-2 {{if .Story.Posted}}border-theme-{{.Theme}} glow-theme-{{.Theme}}{{else}}border-[color:var(--ink)]/10{{end}} shadow-pete overflow-hidden hover:-translate-y-1 hover:shadow-pete-lg transition">
|
||||
{{if .Story.ImageURL}}
|
||||
<div class="aspect-[16/10] w-full overflow-hidden bg-[color:var(--ink)]/5">
|
||||
|
||||
@@ -49,6 +49,16 @@
|
||||
</svg>
|
||||
<span class="sr-only">Feed settings</span>
|
||||
</button>
|
||||
<button type="button" data-reader-open
|
||||
title="Feed mode — read one story at a time (press f)"
|
||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
||||
<path d="M2 4h6a3 3 0 0 1 3 3v13a2.5 2.5 0 0 0-2.5-2.5H2z"></path>
|
||||
<path d="M22 4h-6a3 3 0 0 0-3 3v13a2.5 2.5 0 0 1 2.5-2.5H22z"></path>
|
||||
</svg>
|
||||
<span class="sr-only">Feed mode</span>
|
||||
</button>
|
||||
{{if .Weather.Variant}}
|
||||
<button type="button" data-weather-toggle aria-pressed="true"
|
||||
title="Toggle weather animation"
|
||||
@@ -145,6 +155,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pete-reader" class="hidden fixed inset-0 z-50" aria-modal="true" role="dialog" aria-label="Feed mode reader">
|
||||
<div class="pete-reader-backdrop" data-reader-backdrop></div>
|
||||
<div class="pete-reader-shell">
|
||||
<div class="pete-reader-bar">
|
||||
<span class="pete-reader-progress tabular-nums" data-reader-progress>—</span>
|
||||
<div class="pete-reader-nav">
|
||||
<button type="button" data-reader-prev class="pete-reader-btn" title="Previous (←)" aria-label="Previous article">←</button>
|
||||
<button type="button" data-reader-next class="pete-reader-btn" title="Next (→)" aria-label="Next article">→</button>
|
||||
<a data-reader-link target="_blank" rel="noopener noreferrer" class="pete-reader-btn pete-reader-btn-open" title="Open original (o)">Open ↗</a>
|
||||
<button type="button" data-reader-close class="pete-reader-btn" title="Close (esc)" aria-label="Close reader">esc</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pete-reader-scroll" data-reader-scroll>
|
||||
<article class="pete-reader-article" data-reader-article></article>
|
||||
</div>
|
||||
<div class="pete-reader-hint">
|
||||
<span><kbd>←</kbd> <kbd>→</kbd> navigate · <kbd>o</kbd> open · <kbd>u</kbd> mark unread · <kbd>esc</kbd> close</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="mx-auto max-w-6xl px-4 pb-24">
|
||||
{{block "main" .}}{{end}}
|
||||
</main>
|
||||
@@ -182,5 +213,6 @@
|
||||
<script src="/static/js/weather-forecast.js" defer></script>
|
||||
<script src="/static/js/search.js" defer></script>
|
||||
<script src="/static/js/settings.js" defer></script>
|
||||
<script src="/static/js/reader.js" defer></script>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
|
||||
Reference in New Issue
Block a user