Compare commits
2 Commits
feat/podca
...
4d875e12d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d875e12d6 | ||
| b496e607fc |
10
AGENTS.md
Normal file
10
AGENTS.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## OpenWiki
|
||||||
|
|
||||||
|
This repository has documentation located in the /openwiki directory.
|
||||||
|
|
||||||
|
Start here:
|
||||||
|
- [OpenWiki quickstart](openwiki/quickstart.md)
|
||||||
|
|
||||||
|
OpenWiki includes repository overview, architecture notes, workflows, domain concepts, operations, integrations, testing guidance, and source maps.
|
||||||
|
|
||||||
|
When working in this repository, read the OpenWiki quickstart first, then follow its links to the relevant architecture, workflow, domain, operation, and testing notes.
|
||||||
11
openwiki/.last-update.json
Normal file
11
openwiki/.last-update.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"updatedAt": "2026-07-02T05:28:44Z",
|
||||||
|
"gitHead": "b496e607fc37e795c765355dd99c191518e6f7f2",
|
||||||
|
"mode": "init",
|
||||||
|
"pages": [
|
||||||
|
"openwiki/quickstart.md",
|
||||||
|
"openwiki/architecture/app-architecture.md",
|
||||||
|
"openwiki/workflows/product-workflows.md",
|
||||||
|
"openwiki/operations/development-and-testing.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
160
openwiki/architecture/app-architecture.md
Normal file
160
openwiki/architecture/app-architecture.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# App Architecture
|
||||||
|
|
||||||
|
This page explains how the major Marks runtime pieces fit together. For product-level flows, see [Product workflows](../workflows/product-workflows.md). For setup and verification, see [Development and testing](../operations/development-and-testing.md).
|
||||||
|
|
||||||
|
## Targets and source layout
|
||||||
|
|
||||||
|
`/project.yml` is the XcodeGen project definition. It defines four targets:
|
||||||
|
|
||||||
|
- `Marks` — the main iOS app. Sources include `/Marks` plus `/MarksWidget/WidgetDataStore.swift` so the app can write widget data.
|
||||||
|
- `ShareExtension` — app extension with `/ShareExtension` plus selected shared model/service files from `/Marks` (`Bookmark`, `ServerConfig`, `LinkdingAPI`, `MarksAuth`, `Log`, `PodcastRequests`).
|
||||||
|
- `MarksWidget` — WidgetKit extension with `/MarksWidget`.
|
||||||
|
- `MarksTests` — unit-test bundle depending on `Marks`.
|
||||||
|
|
||||||
|
The app is Swift 6.0 and targets iOS 26.0. The main app, share extension, and widget extension share the app group `group.com.magicive.marks` through entitlements and runtime code.
|
||||||
|
|
||||||
|
## App entrypoint and navigation shell
|
||||||
|
|
||||||
|
`/Marks/MarksApp.swift` contains the `@main` app and `MainContainer`.
|
||||||
|
|
||||||
|
At startup:
|
||||||
|
|
||||||
|
1. `MarksApp` loads a saved `ServerConfig` or falls back to a built-in default config.
|
||||||
|
2. `MainContainer` constructs `LinkdingAPI(config:)`.
|
||||||
|
3. `MainContainer` stores `BookmarksViewModel(api:cacheKey:)` in `@State`, keeping the model stable across SwiftUI re-renders.
|
||||||
|
4. A top-level `TabView` presents Bookmarks, Tags, Podcasts, and Search.
|
||||||
|
|
||||||
|
`MainContainer` also handles:
|
||||||
|
|
||||||
|
- custom deep links (`marks://bookmark?...`, `marks://podcast?...`),
|
||||||
|
- App Intent handoff through `IntentRouter.shared`,
|
||||||
|
- pending podcast requests queued by the share extension,
|
||||||
|
- foreground refreshes and background podcast progress persistence.
|
||||||
|
|
||||||
|
## Central state: `BookmarksViewModel`
|
||||||
|
|
||||||
|
`/Marks/Views/BookmarksViewModel.swift` is `@Observable @MainActor` and is the core app state object. It owns:
|
||||||
|
|
||||||
|
- loaded bookmarks, errors, loading flags, pagination URL, search query, and unread filter,
|
||||||
|
- generated smart collections and enrichment progress,
|
||||||
|
- a `LinkdingAPI` actor for server sync,
|
||||||
|
- `ClaudeService` for backend AI/podcast calls,
|
||||||
|
- `PodcastPlayerViewModel` and `PodcastGenerationManager`,
|
||||||
|
- the disk bookmark cache URL.
|
||||||
|
|
||||||
|
Key side effects during bookmark loading and mutations:
|
||||||
|
|
||||||
|
- `load()` optionally restores cached bookmarks from Application Support, fetches linkding bookmarks, restores local AI metadata, saves the full unfiltered cache, writes recent bookmarks to `WidgetDataStore`, and indexes bookmarks into Spotlight.
|
||||||
|
- `loadMore()` follows linkding's `next` URL and indexes newly appended bookmarks.
|
||||||
|
- `delete()` and `archive()` mutate linkding, remove local entries, and remove Spotlight entries.
|
||||||
|
- `addBookmark()` saves to linkding, inserts locally, indexes the bookmark, and starts best-effort AI enrichment.
|
||||||
|
- `enrichAll()` and background `startEnrichment()` call AI enrichment, persist AI metadata, and reindex enriched bookmarks.
|
||||||
|
|
||||||
|
Because one `BookmarksViewModel` instance is shared by all tabs, array mutations and filters have cross-tab effects.
|
||||||
|
|
||||||
|
## Linkding integration
|
||||||
|
|
||||||
|
`/Marks/Services/LinkdingAPI.swift` is an actor wrapping linkding's REST API. It builds URLs from `ServerConfig` (`/Marks/Models/ServerConfig.swift`) and sends `Authorization: Token ...` headers.
|
||||||
|
|
||||||
|
Implemented operations include:
|
||||||
|
|
||||||
|
- `fetchBookmarks(search:offset:limit:unread:)` → `GET /api/bookmarks/`,
|
||||||
|
- `fetchBookmarksFromUrl(_:)` for pagination `next` URLs,
|
||||||
|
- `checkBookmark(url:)` → `GET /api/bookmarks/check/`, used by the share extension,
|
||||||
|
- `fetchTags(limit:)` → `GET /api/tags/`,
|
||||||
|
- `createBookmark(_:)` → `POST /api/bookmarks/`,
|
||||||
|
- `updateBookmark(id:update:)` → `PATCH /api/bookmarks/{id}/`,
|
||||||
|
- `deleteBookmark(id:)` → `DELETE /api/bookmarks/{id}/`,
|
||||||
|
- `archiveBookmark(id:)` → `POST /api/bookmarks/{id}/archive/`,
|
||||||
|
- `verifyConnection()` for configuration checks.
|
||||||
|
|
||||||
|
`Bookmark`, `BookmarkResponse`, create/update payloads, `BookmarkCheck`, tag response models, and `SmartCollection` live in `/Marks/Models/Bookmark.swift`.
|
||||||
|
|
||||||
|
## Configuration and app-group sharing
|
||||||
|
|
||||||
|
`ServerConfig` contains linkding host, optional port, path, token, and HTTPS flag. Its extension stores config in two places:
|
||||||
|
|
||||||
|
- `UserDefaults.standard` for the main app,
|
||||||
|
- `UserDefaults(suiteName: "group.com.magicive.marks")` for extensions.
|
||||||
|
|
||||||
|
`ServerConfig.loadShared()` is used by the share extension, which cannot read the main app's standard defaults. Avoid changing config keys or app-group IDs without updating all entitlements, `ServerConfig`, `WidgetDataStore`, and `PodcastRequests` together.
|
||||||
|
|
||||||
|
Security note: `/Marks/MarksApp.swift` currently includes a built-in default config with a token-like value. Treat it as sensitive; do not copy it into documentation or generated output.
|
||||||
|
|
||||||
|
## Backend AI, analytics, and podcasts
|
||||||
|
|
||||||
|
`/Marks/Services/MarksAuth.swift` registers an anonymous device with the Marks backend and caches a JWT in `UserDefaults.standard`. `ClaudeService` uses that token for backend calls:
|
||||||
|
|
||||||
|
- bookmark enrichment: `POST /v1/marks/enrich`,
|
||||||
|
- semantic search: `POST /v1/marks/search`,
|
||||||
|
- smart collections: `POST /v1/marks/collections`,
|
||||||
|
- podcast generation: `POST /v1/podcast/generate`,
|
||||||
|
- podcast polling: `GET /v1/podcast/status/{jobId}`,
|
||||||
|
- podcast audio download: `GET /v1/podcast/audio/{jobId}`.
|
||||||
|
|
||||||
|
`/Marks/Services/AnalyticsService.swift` sends events through the backend path used by `ClaudeService`/`MarksAuth`.
|
||||||
|
|
||||||
|
Podcast audio is downloaded to Application Support under a `podcasts` directory using a stable hash of the article URL. `PodcastIndex` records metadata in `index.json`; `PodcastLibrary` reads that index for the Podcasts tab and widget metadata.
|
||||||
|
|
||||||
|
## On-device Ask and Spotlight
|
||||||
|
|
||||||
|
The app has two AI paths:
|
||||||
|
|
||||||
|
1. **Backend AI** through `ClaudeService` for enrichment, semantic search, smart collections, and podcasts.
|
||||||
|
2. **On-device RAG** through `/Marks/Services/BookmarkAssistant.swift` for the Ask UI.
|
||||||
|
|
||||||
|
`BookmarkAssistant` uses `FoundationModels.LanguageModelSession` with `BookmarkSearchTool`. Its instructions require answers to be grounded in bookmarks. Retrieval flows through Spotlight:
|
||||||
|
|
||||||
|
- `SpotlightIndexer` indexes bookmarks after loads, load-more operations, add, and enrichment.
|
||||||
|
- `SpotlightBookmarkSearch`/`BookmarkSearchTool` search the index for relevant bookmarks.
|
||||||
|
- `BookmarkEntity` also conforms to indexed App Intents concepts for system-level entity discovery.
|
||||||
|
|
||||||
|
If changing bookmark fields, keep `Bookmark`, `BookmarkEntity`, Spotlight indexing, widget payloads, and row UI in sync.
|
||||||
|
|
||||||
|
## Share extension architecture
|
||||||
|
|
||||||
|
The share extension is implemented by `/ShareExtension/ShareViewController.swift` and `/ShareExtension/ShareView.swift`.
|
||||||
|
|
||||||
|
It accepts a shared URL/text payload, creates a SwiftUI save card, loads shared `ServerConfig`, checks linkding for an existing bookmark, and lets the user edit tags, notes, read-later state, and a Create Podcast toggle.
|
||||||
|
|
||||||
|
For suggestions, it combines:
|
||||||
|
|
||||||
|
- linkding `checkBookmark` metadata and `auto_tags`,
|
||||||
|
- the user's existing tag vocabulary from `fetchTags()`,
|
||||||
|
- backend AI tag suggestions through `/ShareExtension/TagSuggester.swift`.
|
||||||
|
|
||||||
|
Existing bookmarks are updated rather than duplicated. New bookmarks are created through `LinkdingAPI`. Podcast generation is not performed inside the extension; it queues a `PodcastRequests` item in the app group for the main app to process later.
|
||||||
|
|
||||||
|
## Widget architecture and deep links
|
||||||
|
|
||||||
|
`/MarksWidget/MarksWidgetBundle.swift` registers three widgets:
|
||||||
|
|
||||||
|
- `RecentBookmarksWidget` (`systemMedium`),
|
||||||
|
- `RandomBookmarkWidget` (`systemSmall`),
|
||||||
|
- `RecentPodcastsWidget` (`systemMedium`).
|
||||||
|
|
||||||
|
Widgets read only app-group JSON through `/MarksWidget/WidgetDataStore.swift`:
|
||||||
|
|
||||||
|
- `widget_bookmarks.json`,
|
||||||
|
- `widget_podcasts.json`.
|
||||||
|
|
||||||
|
They do not call network APIs. Main-app writes trigger `WidgetCenter.shared.reloadAllTimelines()` when WidgetKit is available.
|
||||||
|
|
||||||
|
Widget links use custom URLs built by `URL.marksBookmark(_:)` and `URL.marksPodcast(_:)`. `MainContainer.handleDeepLink(_:)` opens bookmark URLs in `BrowserView` or starts podcast playback and shows the player.
|
||||||
|
|
||||||
|
## App Intents and Shortcuts
|
||||||
|
|
||||||
|
`/Marks/Intents/MarksAppIntents.swift` defines:
|
||||||
|
|
||||||
|
- `OpenBookmarkIntent` — opens a selected `BookmarkEntity` in app.
|
||||||
|
- `SearchMarksIntent` — conforms to the system search schema and routes text to the Search tab.
|
||||||
|
- `AddBookmarkIntent` — saves a URL headlessly through linkding and refreshes widget data.
|
||||||
|
- `ShowUnreadIntent` — fetches up to 10 unread bookmarks.
|
||||||
|
- `SummarizeBookmarkIntent` — calls backend AI enrichment for a selected bookmark.
|
||||||
|
- `MarksShortcuts` — registers Add Bookmark, Search, Unread, and Summarize shortcuts.
|
||||||
|
|
||||||
|
`IntentRouter` bridges intents that need to open UI back into `MainContainer`. `BookmarkEntity` maps between linkding `Bookmark` values and App Intents entities.
|
||||||
|
|
||||||
|
## Recent history context
|
||||||
|
|
||||||
|
Recent git history shows major investment in podcast UX and on-device AI: the latest merge added played/unplayed queues, background podcast generation, share-sheet audio requests, podcast library/index changes, and player improvements. A prior AI commit added on-device RAG, Spotlight indexing, and unified logging. Use those areas cautiously because they span app state, storage, widgets, share extension, and App Intents rather than a single view.
|
||||||
155
openwiki/operations/development-and-testing.md
Normal file
155
openwiki/operations/development-and-testing.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Development and Testing
|
||||||
|
|
||||||
|
This page covers local setup, generated project guidance, runtime configuration/storage, and verification strategy for changes.
|
||||||
|
|
||||||
|
## Local setup
|
||||||
|
|
||||||
|
Requirements from `/README.md` and `/project.yml`:
|
||||||
|
|
||||||
|
- macOS with Xcode capable of building the declared iOS 26 / Swift 6 project.
|
||||||
|
- [XcodeGen](https://github.com/yonaskolb/XcodeGen).
|
||||||
|
- A linkding server with API access for real sync testing.
|
||||||
|
- Access to the Marks backend for AI, semantic search, smart collections, analytics, and podcast generation flows.
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install xcodegen
|
||||||
|
xcodegen generate
|
||||||
|
open Marks.xcodeproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and run the `Marks` scheme in Xcode. `/project.yml` declares schemes for `Marks`, `MarksWidget`, and `ShareExtension`; the `Marks` scheme builds the app, share extension, and widget extension and runs `MarksTests` in the test action.
|
||||||
|
|
||||||
|
## Project generation rules
|
||||||
|
|
||||||
|
Use `/project.yml` as the source of truth for targets, source membership, entitlements, and dependencies. Regenerate the Xcode project after changing target membership:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodegen generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Common target-membership traps:
|
||||||
|
|
||||||
|
- The main app includes `/MarksWidget/WidgetDataStore.swift` directly so it can write widget JSON.
|
||||||
|
- The share extension includes selected shared files from `/Marks/Models` and `/Marks/Services`; if the extension needs new model/service code, add it in `/project.yml`.
|
||||||
|
- The app, widget, and share extension all depend on matching app-group entitlements.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Current automated test coverage is concentrated in `/MarksTests/AppIntentsTests.swift`.
|
||||||
|
|
||||||
|
Covered today:
|
||||||
|
|
||||||
|
- `BookmarkEntity` mapping from `Bookmark`, fallback title behavior, and round-trip fields.
|
||||||
|
- `EntityIdentifier` encoding.
|
||||||
|
- `IntentRouter` open/search setters.
|
||||||
|
- Direct no-network `perform()` tests for Search and Open intents.
|
||||||
|
- App Shortcuts registration count.
|
||||||
|
|
||||||
|
Not currently covered by repository tests:
|
||||||
|
|
||||||
|
- `BookmarksViewModel` load/cache/pagination/mutation flows.
|
||||||
|
- Linkding networking with mocks.
|
||||||
|
- AI enrichment, semantic search, smart collections, or podcast backend calls.
|
||||||
|
- Share extension save/update/tag suggestion flows.
|
||||||
|
- Widget timeline rendering/deep links.
|
||||||
|
- Browser reading progress/reader mode.
|
||||||
|
- Podcast generation, playback, queue, played/unplayed, and background audio behavior.
|
||||||
|
|
||||||
|
Suggested test command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild test -scheme Marks -destination 'platform=iOS Simulator,name=iPhone 16'
|
||||||
|
```
|
||||||
|
|
||||||
|
Use an installed simulator name if `iPhone 16` is unavailable.
|
||||||
|
|
||||||
|
## Manual verification checklist by area
|
||||||
|
|
||||||
|
Bookmark sync/list changes:
|
||||||
|
|
||||||
|
- Launch with valid linkding config.
|
||||||
|
- Confirm cached bookmarks render quickly, then network refresh updates the list.
|
||||||
|
- Test pull-to-refresh, pagination, unread filter, add, edit, archive, delete.
|
||||||
|
- Confirm widget bookmark JSON updates and Spotlight indexing still happens.
|
||||||
|
|
||||||
|
Search/tag changes:
|
||||||
|
|
||||||
|
- Test linkding search and semantic search separately.
|
||||||
|
- Verify unread filter interactions.
|
||||||
|
- Confirm Tags tab counts include expected linkding and AI tags.
|
||||||
|
|
||||||
|
AI changes:
|
||||||
|
|
||||||
|
- Test enrichment on add, background enrichment, Enrich All, semantic search, Smart Collections, and Summarize intent if affected.
|
||||||
|
- Test Ask on a device/simulator environment where Apple on-device model availability can be observed; handle unavailable states.
|
||||||
|
- Preserve backend privacy expectations: URLs/titles/tags/summaries may be sent to the Marks backend for backend AI paths.
|
||||||
|
|
||||||
|
Share extension changes:
|
||||||
|
|
||||||
|
- Share a new URL and an already-saved URL.
|
||||||
|
- Verify duplicate-aware update behavior.
|
||||||
|
- Test linkding auto-tags, AI suggestion chips, tag parsing, notes, read-later, and Create Podcast.
|
||||||
|
- Confirm the extension works when shared config is missing; current UI shows an "Open Marks first" style failure.
|
||||||
|
|
||||||
|
Podcast changes:
|
||||||
|
|
||||||
|
- Generate from bookmark row and Browser view.
|
||||||
|
- Start generation while another podcast is playing; confirm background generation does not interrupt playback.
|
||||||
|
- Test queue playback, played/unplayed state, delete, sleep timer, saved speed, background audio, remote controls, now-playing metadata, relaunch restore, and widget recent podcasts.
|
||||||
|
- Share a URL with Create Podcast and confirm the main app drains and starts the queued request.
|
||||||
|
|
||||||
|
Widget/deep-link changes:
|
||||||
|
|
||||||
|
- Confirm `widget_bookmarks.json` and `widget_podcasts.json` are refreshed by main-app writes.
|
||||||
|
- Test Recent Bookmarks, Random Bookmark, and Recent Podcasts widgets.
|
||||||
|
- Tap bookmark and podcast widget entries and verify `marks://bookmark` and `marks://podcast` routing in `MainContainer`.
|
||||||
|
|
||||||
|
App Intents changes:
|
||||||
|
|
||||||
|
- Run `MarksTests`.
|
||||||
|
- Test Shortcuts/Siri flows for Add Bookmark, Search, Show Unread, Summarize, and Open Bookmark.
|
||||||
|
- Verify entity queries still fetch/resolve bookmarks correctly with a configured linkding server.
|
||||||
|
|
||||||
|
## Configuration and persistence map
|
||||||
|
|
||||||
|
Do not read or document live secret values.
|
||||||
|
|
||||||
|
Main app / standard defaults:
|
||||||
|
|
||||||
|
- `serverConfig` — linkding config saved by `ServerConfig.save()`.
|
||||||
|
- `marksDeviceId`, `marksJWT`, `marksJWTExpiry` — Marks backend auth state from `MarksAuth`.
|
||||||
|
- `aiData` — AI summaries/tags by bookmark ID.
|
||||||
|
- `aiSummaryByURL` — summary mirror used outside the main bookmark array.
|
||||||
|
- `podcastProgress`, `podcastPlaybackSpeed`, `podcastSession` — podcast playback state.
|
||||||
|
- `readingProgress` — in-browser reading progress.
|
||||||
|
|
||||||
|
App-group defaults/files (`group.com.magicive.marks`):
|
||||||
|
|
||||||
|
- `serverConfig` — shared linkding config for extensions.
|
||||||
|
- `pendingPodcastRequests` — share extension to main app podcast queue.
|
||||||
|
- `podcastAutoTags` — tags that auto-enable podcast creation in the share extension.
|
||||||
|
- `widget_bookmarks.json` — recent bookmark widget cache.
|
||||||
|
- `widget_podcasts.json` — recent podcast widget cache.
|
||||||
|
|
||||||
|
Application Support files:
|
||||||
|
|
||||||
|
- `bookmarks_cache.json` or `bookmarks_{host}.json` — cached bookmarks.
|
||||||
|
- `podcasts/{hash}.mp3` — generated podcast audio.
|
||||||
|
- `podcasts/index.json` — podcast metadata and played state.
|
||||||
|
|
||||||
|
## Known caveats and stale-doc risks
|
||||||
|
|
||||||
|
- `/Marks/MarksApp.swift` contains a built-in default linkding configuration with a token-like value. Treat it as sensitive and consider replacing it with a safer onboarding/configuration flow before distribution.
|
||||||
|
- `OnboardingView` exists but is not the active first-launch path in `MarksApp` as currently written.
|
||||||
|
- Disconnect behavior currently resets via the default config callback path instead of clearly deleting config and returning to onboarding.
|
||||||
|
- README AI provider text appears older than source: current backend AI/podcast features go through `MarksAuth` + `ClaudeService`, while Ask uses Apple's on-device `FoundationModels`.
|
||||||
|
- `MarksAuth` stores backend JWT/device state in `UserDefaults`, not Keychain.
|
||||||
|
- App-group ID strings are duplicated in code and entitlements; update all occurrences together if changing it.
|
||||||
|
- Share-extension podcast queue draining clears requests before generation success, so crash/retry semantics deserve care.
|
||||||
|
- Recent git history shows podcast and on-device AI features were added across many files; avoid narrow changes that ignore cross-target data flow.
|
||||||
|
|
||||||
|
## Existing design guidance
|
||||||
|
|
||||||
|
`/docs/design-wwdc26.md` is a useful source for UI direction. It emphasizes native iOS 26 UI/Liquid Glass behavior, editorial bookmark rows as the brand canvas, Dynamic Type-safe row layout, bottom/reachable search patterns, WidgetKit app-group data, and standard platform affordances. Link to it rather than duplicating the full design brief.
|
||||||
81
openwiki/quickstart.md
Normal file
81
openwiki/quickstart.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Marks OpenWiki Quickstart
|
||||||
|
|
||||||
|
Marks is a native SwiftUI iOS client for [linkding](https://github.com/sissbruecker/linkding), a self-hosted bookmark manager. The app targets iOS 26 and Swift 6, and includes a main app, share extension, widget extension, App Intents, AI-assisted bookmark features, and podcast generation/playback.
|
||||||
|
|
||||||
|
Start here when changing the repository, then follow the page links below for the area you are touching.
|
||||||
|
|
||||||
|
## What this repository contains
|
||||||
|
|
||||||
|
- **Main iOS app** in `/Marks` with SwiftUI tabs for bookmarks, tags, podcasts, and search. The entrypoint is `/Marks/MarksApp.swift`.
|
||||||
|
- **Models** in `/Marks/Models`, centered on `Bookmark`, linkding response/create/update payloads, tags, smart collections, and `ServerConfig`.
|
||||||
|
- **Services** in `/Marks/Services` for linkding networking, AI/backend calls, auth, on-device bookmark Q&A, Spotlight indexing, widget/podcast data, analytics, and logging.
|
||||||
|
- **Views** in `/Marks/Views` for the primary product surfaces. `BookmarksViewModel` is the central app state object.
|
||||||
|
- **App Intents** in `/Marks/Intents` for opening, searching, adding, listing unread, and summarizing bookmarks.
|
||||||
|
- **Share extension** in `/ShareExtension` for saving shared URLs/text to linkding and queueing podcast generation.
|
||||||
|
- **Widget extension** in `/MarksWidget` for recent bookmarks, random bookmark, and recent podcast widgets.
|
||||||
|
- **Tests** in `/MarksTests`, currently focused on the App Intents layer.
|
||||||
|
- **XcodeGen project spec** in `/project.yml`; `Marks.xcodeproj` is generated/checked in but `project.yml` is the project definition to inspect first.
|
||||||
|
- **Design notes** in `/docs/design-wwdc26.md` for iOS 26/Liquid Glass, search, widgets, and row-design guidance.
|
||||||
|
|
||||||
|
## Major documentation pages
|
||||||
|
|
||||||
|
- [Architecture notes](architecture/app-architecture.md) — target layout, state flow, networking, persistence, AI, Spotlight, extensions, widgets, and App Intents.
|
||||||
|
- [Product workflows](workflows/product-workflows.md) — how bookmark browsing, saving, AI enrichment, Ask, smart collections, podcasts, widgets, and intents behave.
|
||||||
|
- [Development and testing](operations/development-and-testing.md) — setup/build/test commands, generated project guidance, storage/config map, verification checklist, and caveats.
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
The README documents the intended local setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install xcodegen
|
||||||
|
xcodegen generate
|
||||||
|
open Marks.xcodeproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build and run from Xcode using the `Marks` scheme. `/project.yml` defines Swift 6.0, iOS 26.0 deployment target, the `Marks`, `ShareExtension`, `MarksWidget`, and `MarksTests` targets, and the app-group entitlements used by all app surfaces.
|
||||||
|
|
||||||
|
Useful checks before shipping a change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodegen generate
|
||||||
|
xcodebuild test -scheme Marks -destination 'platform=iOS Simulator,name=iPhone 16'
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the simulator name to what is installed locally.
|
||||||
|
|
||||||
|
## Runtime architecture in one minute
|
||||||
|
|
||||||
|
At launch, `MarksApp` loads a `ServerConfig`, constructs `LinkdingAPI`, and stores a shared `BookmarksViewModel` in `MainContainer` so it survives SwiftUI re-renders (`/Marks/MarksApp.swift`, `/Marks/Views/BookmarksViewModel.swift`). The top-level `TabView` has Bookmarks, Tags, Podcasts, and Search tabs.
|
||||||
|
|
||||||
|
`BookmarksViewModel` fetches paginated bookmarks from linkding through `LinkdingAPI`, restores local AI metadata, caches bookmarks to Application Support, writes recent bookmark metadata for widgets, and indexes bookmarks into Spotlight. It also owns `ClaudeService`, `PodcastPlayerViewModel`, and `PodcastGenerationManager`, so AI, podcast, search, and list state are shared across tabs.
|
||||||
|
|
||||||
|
Extensions and widgets use the shared app group `group.com.magicive.marks`. `ServerConfig.save()` writes to standard defaults and the app-group defaults; `WidgetDataStore` writes app-group JSON files for widgets; `PodcastRequests` uses app-group defaults to hand podcast requests from the share extension to the main app.
|
||||||
|
|
||||||
|
## External systems and privacy boundaries
|
||||||
|
|
||||||
|
- **linkding** is the source of truth for bookmarks and tags. `LinkdingAPI` uses token auth and calls linkding bookmark, check, tag, create, update, delete, archive, and connection-test endpoints.
|
||||||
|
- **Marks backend** is used by `MarksAuth`/`ClaudeService` for AI enrichment, semantic search, smart collections, analytics events, and podcast generation/audio download.
|
||||||
|
- **Apple on-device intelligence and Spotlight** power the in-app Ask surface via `FoundationModels`, `CoreSpotlight`, and the `BookmarkSearchTool` retrieval path.
|
||||||
|
- **WidgetKit, App Intents, AVFoundation, and MediaPlayer** support widgets, Siri/Shortcuts integration, podcast playback, background audio, now-playing metadata, and remote controls.
|
||||||
|
|
||||||
|
Do not document or expose secret values. `/Marks/MarksApp.swift` currently contains a built-in default server configuration with a token-like value; treat it as sensitive source material and avoid copying it into docs, logs, screenshots, or tests.
|
||||||
|
|
||||||
|
## Important caveats for future agents
|
||||||
|
|
||||||
|
- The README says optional AI features use OpenRouter/Gemini directly. Current source routes backend AI/podcast calls through `MarksAuth` + `ClaudeService`; the in-app Ask feature is on-device via `FoundationModels`. Prefer source evidence when changing AI behavior.
|
||||||
|
- `OnboardingView` exists, but current launch flow uses saved-or-default config directly. Do not assume onboarding is active without changing `/Marks/MarksApp.swift`.
|
||||||
|
- Settings' disconnect callback currently resets to the built-in default config path rather than forcing onboarding. Confirm intended product behavior before changing account/config flows.
|
||||||
|
- `BookmarksViewModel` is shared across tabs, so changes to bookmark arrays, filters, search, enrichment, and pagination can affect Bookmarks, Tags, Search, Podcasts, widgets, and Spotlight indexing.
|
||||||
|
- Share-extension podcast requests are queued in shared defaults and drained by the main app on launch/foreground. Be careful with crash/retry semantics when touching that flow.
|
||||||
|
- Existing automated tests cover App Intents only. For UI, networking, sync, share extension, widgets, and podcasts, add tests where feasible and perform manual verification.
|
||||||
|
|
||||||
|
## Where to start for common changes
|
||||||
|
|
||||||
|
- Bookmark list, pagination, search, tags, enrichment state: `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/BookmarksView.swift`, `/Marks/Services/LinkdingAPI.swift`.
|
||||||
|
- Linkding API changes: `/Marks/Services/LinkdingAPI.swift`, `/Marks/Models/Bookmark.swift`, `/ShareExtension/ShareView.swift`, `/Marks/Intents/IntentSupport.swift`.
|
||||||
|
- AI/backend changes: `/Marks/Services/ClaudeService.swift`, `/Marks/Services/MarksAuth.swift`, `/Marks/Services/BookmarkAssistant.swift`, `/Marks/Services/BookmarkSearchTool.swift`.
|
||||||
|
- Podcast generation/playback: `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Views/BookmarksViewModel.swift`.
|
||||||
|
- Share extension: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, plus shared files declared in `/project.yml`.
|
||||||
|
- Widgets/deep links: `/MarksWidget/WidgetDataStore.swift`, `/MarksWidget/*Widget.swift`, `/Marks/MarksApp.swift`.
|
||||||
|
- App Intents/Siri/Shortcuts: `/Marks/Intents/*`, `/MarksTests/AppIntentsTests.swift`.
|
||||||
149
openwiki/workflows/product-workflows.md
Normal file
149
openwiki/workflows/product-workflows.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user