150 lines
9.3 KiB
Markdown
150 lines
9.3 KiB
Markdown
# Product Workflows
|
|
|
|
This page describes the user-facing behavior implemented by the app and the source files that own each flow.
|
|
|
|
## Browse bookmarks
|
|
|
|
Primary sources: `/Marks/Views/BookmarksView.swift`, `/Marks/Views/BookmarkListRow.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Services/LinkdingAPI.swift`.
|
|
|
|
The Bookmarks tab loads linkding bookmarks through `BookmarksViewModel.load()`. The view supports loading skeletons, pull-to-refresh, pagination as the user scrolls, an unread filter, and row actions.
|
|
|
|
A bookmark row displays title/domain/date context, unread state, available excerpt/AI summary, tags/AI tags, reading progress, and podcast-cache state. Tapping opens `BrowserView`. Swipe/context actions support archive, delete, edit, Safari open, and podcast conversion.
|
|
|
|
Change guidance:
|
|
|
|
- Keep row actions aligned across `BookmarksView`, `BookmarkListRow`, `BookmarkRow`, and `BrowserView`.
|
|
- `BookmarksViewModel.saveToCache(_:)` intentionally skips saving when the unread filter is active, so filtered results do not replace the full cache.
|
|
- Pagination uses linkding's `next` URL through `fetchBookmarksFromUrl(_:)`; do not rebuild next URLs unless the linkding API contract changes.
|
|
|
|
## Add and edit bookmarks in app
|
|
|
|
Primary sources: `/Marks/Views/AddBookmarkView.swift`, `/Marks/Views/EditBookmarkView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Models/Bookmark.swift`.
|
|
|
|
The Add view collects URL, optional title, and comma-separated tags, normalizes/validates the URL, and calls `BookmarksViewModel.addBookmark(url:title:tags:)`. Added bookmarks are inserted locally, indexed into Spotlight, and enriched in a background task.
|
|
|
|
The Edit view updates URL, title, description, tags, unread, and shared state through `BookmarkUpdate`. `BookmarksViewModel.updateBookmark(_:)` preserves locally stored AI summary/tags when replacing the bookmark returned from linkding.
|
|
|
|
## Search and semantic search
|
|
|
|
Primary sources: `/Marks/Views/SearchView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Services/ClaudeService.swift`.
|
|
|
|
The Search tab has two paths:
|
|
|
|
- normal search through linkding's bookmark API (`q` parameter),
|
|
- semantic search through `ClaudeService.semanticSearch(query:in:)`, which fetches up to 200 bookmarks and sends a compact list to the backend for ranking.
|
|
|
|
`SearchMarksIntent` routes spoken/system search text into `IntentRouter`, `MainContainer` switches to the Search tab, and `SearchView` consumes the router text into its visible search field.
|
|
|
|
Change guidance:
|
|
|
|
- Search state lives on the shared `BookmarksViewModel`; search and unread filters can affect list state outside Search.
|
|
- Backend semantic search sends bookmark titles/domains and optional summaries to the Marks backend. Preserve that privacy boundary in UI copy and docs.
|
|
|
|
## Tags
|
|
|
|
Primary sources: `/Marks/Views/TagsView.swift`, `/Marks/Models/Bookmark.swift`.
|
|
|
|
The Tags tab derives tag counts from both linkding tags (`Bookmark.tagNames`) and local AI tags (`Bookmark.aiTags`), sorted by count/name. Tapping a tag shows matching bookmarks and reuses bookmark row/browser flows.
|
|
|
|
If changing tag semantics, check in-app Add/Edit, share extension tag parsing, linkding `fetchTags()`, AI suggestions, Tags tab counts, and App Intents bookmark entity fields.
|
|
|
|
## AI enrichment and smart collections
|
|
|
|
Primary sources: `/Marks/Services/ClaudeService.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/CollectionsView.swift`, `/Marks/Services/AISummaryStore.swift`.
|
|
|
|
AI enrichment generates a short summary and tags for a bookmark. The app stores AI metadata locally in `UserDefaults.standard` under `aiData`, keyed by bookmark ID, and mirrors summaries by URL through `AISummaryStore` for podcast/list display.
|
|
|
|
Enrichment can happen in three ways:
|
|
|
|
- after adding a bookmark,
|
|
- as a silent background pass over up to five loaded bookmarks without summaries,
|
|
- manually through Enrich All.
|
|
|
|
Smart Collections send up to 150 bookmarks to the backend and render the returned collection names, descriptions, and bookmark IDs in `CollectionsView`.
|
|
|
|
Caveat: README language mentions OpenRouter/Gemini directly, but current source uses the Marks backend proxy for these calls.
|
|
|
|
## Ask your bookmarks
|
|
|
|
Primary sources: `/Marks/Views/AskView.swift`, `/Marks/Services/BookmarkAssistant.swift`, `/Marks/Services/BookmarkSearchTool.swift`, `/Marks/Services/SpotlightBookmarkSearch.swift`, `/Marks/Services/SpotlightIndexer.swift`.
|
|
|
|
Ask is an on-device RAG workflow. `BookmarkAssistant` creates a `FoundationModels` language model session with a bookmark search tool. The model instructions say bookmarks are the only source of truth and require the tool for lookup.
|
|
|
|
The retrieval corpus comes from Spotlight. `BookmarksViewModel` indexes bookmarks after loading, loading more, adding, and enrichment. Deleted/archived bookmarks are removed from Spotlight.
|
|
|
|
Change guidance:
|
|
|
|
- If Ask gives stale answers, inspect Spotlight indexing and removal paths first.
|
|
- If changing indexed fields, update `BookmarkEntity`, `SpotlightIndexer`, search tooling, and tests where relevant.
|
|
- The Ask path is separate from backend semantic search and does not use `ClaudeService`.
|
|
|
|
## Share extension save flow
|
|
|
|
Primary sources: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, `/Marks/Services/PodcastRequests.swift`, `/project.yml`.
|
|
|
|
The share extension extracts a URL/text payload and shows an interactive save card. On load it:
|
|
|
|
1. reads linkding config from app-group defaults,
|
|
2. calls `LinkdingAPI.checkBookmark(url:)`,
|
|
3. pre-fills existing bookmark data or scraped metadata/linkding auto-tags,
|
|
4. fetches existing tag vocabulary and backend AI tag suggestions,
|
|
5. lets the user edit tags, notes, read-later state, and Create Podcast.
|
|
|
|
On save, existing bookmarks are patched; new bookmarks are created. If Create Podcast is enabled, or if configured auto-tags match, the extension enqueues a `PodcastRequests` item instead of generating audio itself.
|
|
|
|
Change guidance:
|
|
|
|
- The extension is short-lived. Keep long-running work in the main app.
|
|
- The extension depends on shared source files listed in `/project.yml`; adding service dependencies may require target membership updates.
|
|
- `ShareExtension/Info.plist` currently uses a broad activation rule. Narrowing supported payloads is a product decision.
|
|
|
|
## Podcasts
|
|
|
|
Primary sources: `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Services/PodcastLibrary.swift`, `/Marks/Services/ClaudeService.swift`, `/Marks/Services/PodcastRequests.swift`, `/Marks/Info.plist`.
|
|
|
|
Podcast generation converts an article URL into audio through the Marks backend:
|
|
|
|
1. create generation job (`/v1/podcast/generate`),
|
|
2. poll status every few seconds,
|
|
3. download audio when done,
|
|
4. save MP3 under Application Support `podcasts/`,
|
|
5. upsert `PodcastIndex`,
|
|
6. reload `PodcastLibrary`,
|
|
7. write recent podcast metadata for widgets.
|
|
|
|
`BookmarksViewModel.playOrGeneratePodcast(...)` prevents interrupting current playback. If the player is idle, it starts foreground generation/playback and callers present the player. If busy, it enqueues background generation through `PodcastGenerationManager`.
|
|
|
|
The player uses AVFoundation/MediaPlayer for playback, background audio, progress, saved playback speed, restored paused session, queue playback, now-playing metadata, remote controls, sleep timer, and played/unplayed state.
|
|
|
|
Change guidance:
|
|
|
|
- The main app plist enables background audio and the custom `marks` URL scheme.
|
|
- Widget podcast metadata is in the app group, but audio files and `PodcastIndex` are in the main app's Application Support directory.
|
|
- Share-extension queued podcast requests are drained by `BookmarksViewModel.processPendingPodcastRequests()` on launch/foreground. Consider retry semantics before changing drain behavior.
|
|
|
|
## Widgets and deep links
|
|
|
|
Primary sources: `/MarksWidget/WidgetDataStore.swift`, `/MarksWidget/RecentBookmarksWidget.swift`, `/MarksWidget/RandomBookmarkWidget.swift`, `/MarksWidget/RecentPodcastsWidget.swift`, `/Marks/MarksApp.swift`.
|
|
|
|
Widgets are offline readers of app-group JSON data:
|
|
|
|
- Recent Bookmarks shows up to three recent bookmarks and links rows to `marks://bookmark`.
|
|
- Random Bookmark chooses a random cached bookmark and links the whole widget to `marks://bookmark`.
|
|
- Recent Podcasts shows up to three podcasts and links rows to `marks://podcast`.
|
|
|
|
The main app handles bookmark links by presenting `BrowserView`; podcast links start playback and show `PodcastPlayerView`.
|
|
|
|
## App Intents, Siri, and Shortcuts
|
|
|
|
Primary sources: `/Marks/Intents/MarksAppIntents.swift`, `/Marks/Intents/BookmarkEntity.swift`, `/Marks/Intents/IntentSupport.swift`, `/Marks/Intents/BookmarkOnscreen.swift`, `/MarksTests/AppIntentsTests.swift`.
|
|
|
|
Implemented intents:
|
|
|
|
- Open Bookmark — routes a selected entity URL into the app browser.
|
|
- Search Marks — system search schema; opens Search tab with criteria text.
|
|
- Add Bookmark — headless linkding save and widget refresh.
|
|
- Show Unread Bookmarks — fetches up to ten unread items.
|
|
- Summarize Bookmark — backend AI summary for a selected bookmark.
|
|
|
|
`BookmarkEntity` maps linkding bookmarks into App Intents entities. Browser presentations annotate current bookmarks for on-screen entity references. Tests currently cover entity mapping, router behavior, direct no-network open/search intent perform calls, and shortcut registration.
|