From af6eae9baf2ab489d4636f1833a51eb996f69a32 Mon Sep 17 00:00:00 2001 From: rays Date: Wed, 14 Oct 2026 15:52:22 +0000 Subject: [PATCH] Page type-check: types for what the API sends, and for S (#235) web/src/types.ts mirrors FeedRow, EntryRow, EncRow, /api/me and PopularRow; types only, so swc strips it and it is not in build.mjs's list. Co-Authored-By: Claude Opus 5.5 --- web/src/types.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++++ web/src/util.ts | 2 +- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 web/src/types.ts diff --git a/web/src/types.ts b/web/src/types.ts new file mode 100644 index 0000000..8159d33 --- /dev/null +++ b/web/src/types.ts @@ -0,0 +1,115 @@ +// What ipx's API sends the page, as types for the type-check (#235), each named after the Rust +// struct it mirrors in crates/ipx/src. Types only: swc strips this file to nothing, so it is not +// in web/build.mjs's list; tsconfig.json's glob reads it. A field added there goes here too. +// An Option is `T | null`, as serde writes None. + +/// A feed in your list, `/api/feeds` and the live `feed_row` (web.rs FeedRow). +interface Feed { + id: string; + url: string; + title: string | null; + image: string | null; + folder: string | null; + category: string | null; + feed_category: string | null; + keywords: string[]; + blocked: string[]; + deadair: boolean; + deadair_pause: number; + allow_explicit: boolean; + auto_download: boolean; + max_new_per_check: number | null; + /// The OPML subscription it came from. + group: string | null; + orphaned: boolean; + managed: boolean; + schedule: string | null; + schedule_mins: number | null; + every_mins: number; + last_checked: number | null; + next_check: number | null; + last_error: string | null; + failing: {reason: string, new_url: string | null} | null; + entries: number; + downloaded: number; + unread: number; + subscribers: number; + pinned: boolean; + private: boolean; + private_by_address: boolean; +} + +/// An item, in a page of `/api/entries` (db.rs EntryRow). +interface Entry { + guid: string; + feed_id: string; + title: string | null; + link: string | null; + published: number | null; + description: string | null; + read: boolean; + flagged: boolean; + image: string | null; + duration: number | null; + episode: number | null; + season: number | null; + position: number; + enclosures: Enclosure[]; +} + +/// An item's file (db.rs EncRow). +interface Enclosure { + id: number; + /// Seconds DeadAir's copy saves, when you play it from one. + deadair?: number | null; + deadair_pending?: boolean; + feed_id: string; + guid: string; + url: string; + mime: string | null; + length: number | null; + path: string | null; + state: string; + last_error: string | null; +} + +/// `/api/me`. +interface Me { + name: string; + admin: boolean; + sign_out: string; + blocked: string[]; + deadair_saved: number; + theme: {name: string, mode: string} | null; + version: string; +} + +/// A feed as the Directory lists it (web.rs PopularRow): never its address. +interface DirFeed { + id: string; + title: string | null; + image: string | null; + subscribers: number; + subscribed: boolean; + /// Its id in your list, when it is yours. + feed: string | null; + category: string | null; + subcategory: string | null; + podcast: boolean; +} + +/// The page's own state (util.ts `S`). +interface State { + feeds: Feed[]; + /// The feed or place open: a feed's id, or a view such as ':all'. + feed: string | null; + entries: Entry[]; + total: number; + offset: number; + filter: string; + q: string; + /// The selected item's guid. + sel: string | null; + me: Me | null; + sort: {col: string, dir: 'asc' | 'desc'}; +} diff --git a/web/src/util.ts b/web/src/util.ts index 017aaff..ba0d927 100644 --- a/web/src/util.ts +++ b/web/src/util.ts @@ -215,7 +215,7 @@ function folderArt(f,kids){ function nav(on){ $('#sidebar').classList.toggle('open',on); $('#scrim').hidden=!on; } /* ---------------- state ---------------- */ -const S = { +const S: State = { feeds:[], // Which feed (or place) and which tab were open last time, so a refresh lands back where // you were instead of jumping to the first feed alphabetically.