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>
- SpotlightIndexer: actually push BookmarkEntity into the Spotlight index via
indexAppEntities (IndexedEntity conformance alone indexes nothing). Wired into
every sync/mutation point. This is what lets Apple Intelligence answer
free-form Siri questions grounded in the user's bookmarks.
- On-device RAG: BookmarkAssistant (LanguageModelSession) + BookmarkSearchTool
+ SpotlightBookmarkSearch (CSSearchQuery retrieval), surfaced via AskView with
lightweight Markdown rendering of answers.
- Log: os.Logger facility (com.magicive.marks) replacing ad-hoc print(); shared
with ShareExtension. ClaudeService now logs server error bodies on non-2xx.
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>
AnalyticsService.track() is fire-and-forget — posts to POST /v1/marks/events
which records a Langfuse trace. No external SDK, no Sentry on iOS side.
Events tracked:
- podcast.started (url, cached)
- podcast.completed (url, durationSec) — fires at 97% playback
- podcast.failed (url, error)
- search.semantic (query, resultCount)
- enrich.completed (count)
- collections.generated (collectionCount)
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>
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>
- Edit bookmark: full PATCH via swipe action or long-press context menu;
EditBookmarkView covers URL, title, description, tags, and unread toggle
- Offline cache: bookmarks persisted to ApplicationSupport JSON and shown
instantly on launch; cache is per-server, skipped when unread filter is on
- Unread filter: toolbar toggle sends ?unread=true to the API; title updates
to "Unread"; no stale-cache flash when toggling
- Extract normalizedURL and parsedTags into String+Helpers, removing three
copies of URL normalization and two of tag parsing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete ground-up rewrite in SwiftUI targeting iOS 26. Drops the Flutter/Dart
codebase entirely in favour of a lean native app with no third-party dependencies.
Features shipped:
- Bookmark list with pagination, pull-to-refresh, swipe delete/archive
- Add bookmark form + iOS share extension (zero-tap save from any app)
- Tags tab — all tags sorted by count, tap to browse filtered bookmarks
- Native search tab (Tab role: .search) with instant client-side filtering
- AI enrichment via OpenRouter (google/gemini-2.0-flash-lite-001): auto-summary
and tag generation, semantic search, smart collections
- Settings: linkding server config, OpenRouter API key
- App Groups for credential sharing between main app and share extension
- Swift 6 strict concurrency throughout (@Observable, @MainActor)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>