Turns the Podcasts tab into a proper podcast-app experience across three
features that compose on a shared background-generation service.
Background generation (#2)
- New PodcastGenerationManager runs generate→poll→download off the player,
so producing a new episode never stops current playback.
- Headphone taps never interrupt: idle → generate-and-play in the foreground
(unchanged); already playing → generate in the background, episode lands in
the library. A "Generating…" banner surfaces active jobs.
Played/unplayed + queue (#1)
- PodcastEntry gains playedAt (old index.json decodes as unplayed).
- Podcasts tab splits into Up Next / Played with a Play All queue that
auto-advances; finishing marks played; swipe to toggle played state.
Share-extension audio (#3)
- "Create podcast" toggle on the save card, plus configurable Auto-Podcast
Tags in Settings. The extension enqueues a request into the App Group; the
main app drains it on launch/foreground and generates in the background.
Closes#1Closes#2Closes#3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Save serverConfig to App Group on every app launch so ShareExtension
can always find credentials without requiring a disconnect/reconnect flow
- Add MarksWidget target with RandomBookmarkWidget (random saved bookmark,
reloads hourly) using file-based App Group storage to avoid CFPreferences issues
- Extract BookmarkListRow as shared component with editorial typography tweaks
(17pt semibold title, 13pt domain, relative date, 32×32 favicon, italic AI summary,
20pt podcast icon, 16pt horizontal insets)
- Add MarksWidget and ShareExtension Xcode schemes for direct run/debug
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MiniPlayerView: persistent bar above tab bar, shows title/progress,
play/pause and close buttons, slides in when audio starts
- PodcastPlayerViewModel lifted to BookmarksViewModel so it persists
across sheet dismissals — audio keeps playing in background
- Headphone button on every bookmark row (filled = cached, outline = not)
- start() is idempotent: re-tapping same article resumes without restart
- stop() saves playback position before teardown (PodcastProgress)
- setupPlayer restores saved position on next open
- BrowserView keeps its own local VM (stopOnDismiss: true)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without a UI delegate, WKWebView hands target="_blank" and window.open()
links to the system (Safari). createWebViewWith now loads them in the
existing webview instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Device auto-registers on first launch using hardcoded backend URL
and anon key (MarksAuth.swift). 90-day JWT stored in UserDefaults,
refreshed transparently. No settings fields needed — ClaudeService
is always active and non-optional throughout the app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BrowserState: inject scroll-tracking JS after page load, report position
via WKScriptMessageHandler (weak ref to avoid retain cycle)
- BrowserView: thin blue progress bar at top of viewport; saves progress
to UserDefaults on dismiss; restores scroll position on revisit
- BookmarkRow: 2px progress bar below tags for partially-read articles
- BookmarksView: loads ReadingProgress.all() on appear, refreshes when
browser sheet dismisses
- ReadingProgress: static helper for UserDefaults-backed [url: Double] store;
removes entry when article is fully read (≥99%)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tapping a bookmark now opens an in-app WKWebView instead of bouncing to
Safari. Reader mode injects JS to extract article content and rerender
with clean serif typography and dark-mode support. Bottom toolbar has
back/forward, reader toggle, share, and open-in-Safari escape hatch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>