Resolves the two deviations from the episode detail sheet:
- AI summary now shows in the detail sheet. Added a URL-keyed AISummaryStore
that BookmarksViewModel populates as it enriches/loads bookmarks, so the
decoupled Podcasts tab can surface a summary from just the episode URL —
without recoupling to BookmarksViewModel.
- "Open Bookmark" opens the in-app BrowserView as a nested sheet (generator
threaded through PodcastLibraryView / EpisodePickerView), alongside the
existing "Open in Safari" and Share.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New EpisodeDetailView: title, source domain, created date, played badge,
and per-episode actions — Play/Pause, Mark Played/Unplayed, Open Source,
Share, Delete — all routed through the PodcastLibrary store so the badge
stays in sync.
- Reachable by tapping a row (or "Episode Details" in the context menu) in the
Podcasts tab, and by tapping a row in the episode picker.
Note: AI summary is intentionally omitted for now — it's keyed by bookmark id
(not URL), so surfacing it would recouple the deliberately-decoupled Podcasts
tab to BookmarksViewModel. Open Source uses Safari rather than the in-app
browser to avoid sheet-stacking. Both are noted as follow-ups on issue #8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Unread badge on the Podcasts tab via a new observable PodcastLibrary store
that mirrors PodcastIndex; all played/remove/generation mutations route
through it so the count stays live.
- Persist playback speed across sessions (UserDefaults), applied on setup.
- Sleep timer (15/30/45 min or end-of-episode) with a moon menu in the full
player; end-of-episode stops instead of auto-advancing.
- Persist the current episode + Up Next queue and restore a *paused* session on
launch; save position when backgrounding. Audio session is only claimed on
actual playback so a restored session never interrupts other audio.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a long-press context menu on each library row (Play/Pause,
Mark as Played/Unplayed, Delete).
- Add a Mark as Played/Unplayed control next to Share Episode in the full
player, tracking the loaded episode across queue advances.
- Show a green checkmark on played rows, on top of the existing Played
section + dimmed title, so played state reads at a glance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Surface every podcast in PodcastIndex as a top-level tab instead of a
buried sheet. Repurpose PodcastLibraryView into the tab: play/pause toggle
per episode, a now-playing bar that opens the full player, swipe/Edit to
delete. Uses the shared podcastPlayer so playback state is consistent
across tabs. Removes the redundant Podcasts entry from the bookmarks menu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap hardcoded systemGray/Color.blue pill and button fills for the iOS 26
Liquid Glass APIs so controls read native under the new design system:
- BookmarkRow tag chips: glassEffect in a GlassEffectContainer
- Podcast speed button: glassEffect capsule
- Podcast retry + Onboarding Connect: glassProminent button style
Onboarding text-field fills left as systemGray6 (standard adaptive field
fill; glass behind editable text hurts legibility).
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>
Replace the fire-and-forget share toast with an interactive SwiftUI card.
On open it checks linkding's /api/bookmarks/check/ so an already-saved URL
routes to an Update (no more silent duplicates) and a new URL shows a save
form with title, tags, notes, and a read-later toggle.
Tag suggestions are hybrid: fetch the user's existing tag vocabulary
(/api/tags/) and AI tag ideas (/v1/marks/enrich via anonymous device auth),
auto-fill known-vocabulary matches into the field and surface net-new ideas
as tappable chips. Best-effort and non-blocking — failures never block save.
Also:
- Refresh bookmarks when the app returns to the foreground (scenePhase).
- Render bookmark dates as a static relative label instead of the live
ticking RelativeDateTime timer.
- Share LinkdingAPI/Bookmark/ServerConfig/MarksAuth into the ShareExtension
target via project.yml (durable across xcodegen regen) and regenerate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bookmarkOnscreen(_:) publishes the viewed bookmark as the on-screen
entity (NSUserActivity.appEntityIdentifier) so Siri can resolve "this"
/ "summarize this one"; applied to the BrowserView sheets in
BookmarksView and SearchView
- Add MarksTests (XCTest) covering entity mapping, EntityIdentifier,
IntentRouter, and the real perform() of SearchMarksIntent and
OpenBookmarkIntent; wired into the Marks scheme test phase
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose Marks to Siri, Spotlight, and Shortcuts via App Intents:
- BookmarkEntity (AppEntity + IndexedEntity, keyed on the Linkding
server id) with a server-backed EntityStringQuery for live lookup
- Intents: Add, Open (OpenIntent), Search, ShowUnread, and an AI
Summarize intent backed by ClaudeService; AppShortcutsProvider with
spoken phrases; Siri snippet views and dialog
- SearchMarksIntent conforms to the system.search App Schema
(@AppIntent(schema:), ShowInAppSearchResultsIntent) so it is
executable by conversational Siri / Apple Intelligence
- IntentRouter bridges intents (run in the app process) to the SwiftUI
scene for open/search navigation; widgets refreshed after adds
- Link AppIntents.framework so the metadata processor extracts shortcut
phrases and validates the assistant schema
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface a bit of the page content in the list without fetching/parsing pages
ourselves — linkding already returns the scraped metadata in the bookmark
payload. Add Bookmark.contentExcerpt (website_description, falling back to the
user's note) and render it under the title: AI summary takes priority and stays
italic, otherwise the scraped excerpt shows. Rows with neither degrade to the
existing favicon + title layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply WWDC26 design-brief fixes to the bookmark list reading surface:
- Dynamic Type: replace fixed .system(size:) points with text styles
(title .headline, domain/date .footnote, summary .subheadline, tags
.caption, podcast glyph .title3). Rows now scale and wrap with the
user's text-size setting instead of staying fixed.
- Perf: podcastCached was a FileManager.fileExists stat evaluated inside
body — i.e. per row, per scroll frame. Move it to @State populated by a
.task(id:) that stats off the main actor, once per appearance.
- Forgiveness: destructive trailing swipe is now allowsFullSwipe: false so
a single fling can't permanently delete; requires tapping the button.
Adds docs/design-wwdc26.md (consolidated brief + codebase audit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace Button wrapper with onTapGesture so swipe gestures don't compete
with the row tap recognizer and accidentally open bookmarks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Meta line .tertiary → .secondary (more readable domain/date line)
- Tag font 11pt → 12pt
- Reading progress bar: full-width, green, pinned to row bottom edge via overlay
Co-Authored-By: Claude Sonnet 4.6 <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>
- Mini player shows as soon as generation starts (not just when audio ready)
- Idempotency: tapping headphones again during generation is a no-op
- Playback resets to position 0 on completion (standard player behavior)
- Stop button in full player visible during generation
- PodcastLibraryView lists all generated podcasts
- Sparkles menu gains Podcasts entry
- Fix AnalyticsService Sendable error (serialize before Task.detached boundary)
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>
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>
PodcastIndex.swift stores {articleUrl, filename, title, createdAt} in
App Support/podcasts/index.json. Updated on every successful download.
Enables building a podcast library view later.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cache generated MP3 to App Support/podcasts/<hash>.mp3 — re-opening
the same article plays instantly with zero backend calls
- AVAudioSession .playback/.spokenAudio + .allowBluetoothA2DP — routes
audio to AirPods and Bluetooth headsets
- MPRemoteCommandCenter play/pause/seek — AirPods controls + lock screen
- MPNowPlayingInfoCenter — title and scrubber on lock screen
- UIBackgroundModes: audio in Info.plist — playback continues when backgrounded
- Auto-play when player is ready
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UIDevice.current is MainActor-isolated; replaced with a stable UUID
persisted in UserDefaults to avoid the async access.
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>