Full-featured web UI

Rewrites the page around a persistent player (speed, seek, resume,
MediaSession, keyboard shortcuts), artwork, filter tabs, episode search,
pagination and live progress, with modals and toasts replacing prompt()
and a status line.

Backend gains the metadata that makes that possible: feed and episode
artwork, durations, season/episode numbers and playback position, plus
filters, search, totals, mark-all-read, download-latest and OPML over
HTTP. Schema changes arrive through a real migration, since CREATE TABLE
IF NOT EXISTS does nothing to an installed database.

Fixes filtering, which returned 500 whenever no search term was given:
the search clause was dropped while its parameter was still bound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
2026-09-10 01:42:55 +00:00
parent 93b4815d84
commit 5666166769
6 changed files with 1143 additions and 310 deletions

View File

@@ -401,7 +401,7 @@ async fn import(ctx: Ctx, config_path: &std::path::Path, file: &std::path::Path)
}
/// OPML nests feeds inside folder outlines, so this walks the whole tree.
fn collect_outlines(outlines: &[opml::Outline], out: &mut Vec<(String, String)>) {
pub fn collect_outlines(outlines: &[opml::Outline], out: &mut Vec<(String, String)>) {
for o in outlines {
if let Some(url) = &o.xml_url {
let title = o.title.clone().unwrap_or_else(|| o.text.clone());
@@ -565,6 +565,7 @@ async fn scan_one(
etag.as_deref(),
last_modified.as_deref(),
parsed.ttl_mins,
parsed.image.as_deref(),
)?;
let mut scan = Scan::default();