Update OpenWiki for source ingest
All checks were successful
CI / build-and-deploy (push) Successful in 16s
All checks were successful
CI / build-and-deploy (push) Successful in 16s
This commit is contained in:
@@ -7,7 +7,7 @@ This page explains how the major Marks runtime pieces fit together. For product-
|
|||||||
`/project.yml` is the XcodeGen project definition. It defines four targets:
|
`/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.
|
- `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`).
|
- `ShareExtension` — app extension with `/ShareExtension` plus selected shared model/service files from `/Marks` (`Bookmark`, `IngestPayload`, `ServerConfig`, `LinkdingAPI`, `MarksAuth`, `Log`, `PodcastRequests`).
|
||||||
- `MarksWidget` — WidgetKit extension with `/MarksWidget`.
|
- `MarksWidget` — WidgetKit extension with `/MarksWidget`.
|
||||||
- `MarksTests` — unit-test bundle depending on `Marks`.
|
- `MarksTests` — unit-test bundle depending on `Marks`.
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ At startup:
|
|||||||
1. `MarksApp` loads a saved `ServerConfig` or falls back to a built-in default config.
|
1. `MarksApp` loads a saved `ServerConfig` or falls back to a built-in default config.
|
||||||
2. `MainContainer` constructs `LinkdingAPI(config:)`.
|
2. `MainContainer` constructs `LinkdingAPI(config:)`.
|
||||||
3. `MainContainer` stores `BookmarksViewModel(api:cacheKey:)` in `@State`, keeping the model stable across SwiftUI re-renders.
|
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.
|
4. `MainContainer` also creates an `IngestedSourceLibrary` for local source storage.
|
||||||
|
5. A top-level `TabView` presents Bookmarks, Tags, Sources, Podcasts, and Search.
|
||||||
|
|
||||||
`MainContainer` also handles:
|
`MainContainer` also handles:
|
||||||
|
|
||||||
@@ -70,6 +71,19 @@ Implemented operations include:
|
|||||||
|
|
||||||
`Bookmark`, `BookmarkResponse`, create/update payloads, `BookmarkCheck`, tag response models, and `SmartCollection` live in `/Marks/Models/Bookmark.swift`.
|
`Bookmark`, `BookmarkResponse`, create/update payloads, `BookmarkCheck`, tag response models, and `SmartCollection` live in `/Marks/Models/Bookmark.swift`.
|
||||||
|
|
||||||
|
## Local source ingest
|
||||||
|
|
||||||
|
Local source ingest is separate from linkding bookmarks. It is owned by:
|
||||||
|
|
||||||
|
- `/Marks/Models/IngestedSource.swift` — source metadata for text, PDF, and generic file imports.
|
||||||
|
- `/Marks/Services/IngestedSourceStore.swift` — JSON metadata persistence, security-scoped file copying, PDFKit text extraction, plain-text extraction, and local search.
|
||||||
|
- `/Marks/Services/SourceSpotlightIndexer.swift` — Spotlight indexing/removal for source title, body text, tags, kind, and original filename.
|
||||||
|
- `/Marks/Views/SourcesView.swift` — Sources tab list/search, text import, file importer, QuickLook preview, delete, and podcast actions.
|
||||||
|
|
||||||
|
`IngestedSourceStore` writes `Sources/sources.json` and imported originals under `Sources/files/` in Application Support. Text imports store only metadata/body text. File imports copy the selected file into app storage; PDF imports extract text with `PDFDocument`, while other files attempt UTF-8/ASCII extraction.
|
||||||
|
|
||||||
|
Source podcasts use `IngestedSource.podcastURL` (`marks-source://{uuid}`) as the podcast cache/index key and send up to 100,000 characters of `bodyText` to the backend as `text`.
|
||||||
|
|
||||||
## Configuration and app-group sharing
|
## Configuration and app-group sharing
|
||||||
|
|
||||||
`ServerConfig` contains linkding host, optional port, path, token, and HTTPS flag. Its extension stores config in two places:
|
`ServerConfig` contains linkding host, optional port, path, token, and HTTPS flag. Its extension stores config in two places:
|
||||||
@@ -94,7 +108,7 @@ Security note: `/Marks/MarksApp.swift` currently includes a built-in default con
|
|||||||
|
|
||||||
`/Marks/Services/AnalyticsService.swift` sends events through the backend path used by `ClaudeService`/`MarksAuth`.
|
`/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.
|
Podcast generation accepts either a web URL or a local source pseudo-URL. `ClaudeService.generatePodcast(url:title:sourceText:sourceKind:)` always sends `url` and may also send `title`, `text`, and `source_kind`. Podcast audio is downloaded to Application Support under a `podcasts` directory using a stable hash of the article/source URL. `PodcastIndex` records metadata in `index.json`; `PodcastLibrary` reads that index for the Podcasts tab and widget metadata.
|
||||||
|
|
||||||
## On-device Ask and Spotlight
|
## On-device Ask and Spotlight
|
||||||
|
|
||||||
@@ -109,13 +123,13 @@ The app has two AI paths:
|
|||||||
- `SpotlightBookmarkSearch`/`BookmarkSearchTool` search the index for relevant bookmarks.
|
- `SpotlightBookmarkSearch`/`BookmarkSearchTool` search the index for relevant bookmarks.
|
||||||
- `BookmarkEntity` also conforms to indexed App Intents concepts for system-level entity discovery.
|
- `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.
|
If changing bookmark fields, keep `Bookmark`, `BookmarkEntity`, Spotlight indexing, widget payloads, and row UI in sync. If changing source fields, keep `IngestedSource`, `IngestedSourceStore`, `SourceSpotlightIndexer`, Sources UI, and source podcast generation in sync.
|
||||||
|
|
||||||
## Share extension architecture
|
## Share extension architecture
|
||||||
|
|
||||||
The share extension is implemented by `/ShareExtension/ShareViewController.swift` and `/ShareExtension/ShareView.swift`.
|
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.
|
It accepts shared URL, HTML, RTF, plain-text, and text payloads, uses `IngestPayloadParser` to find the first bookmarkable `http`/`https` URL and preserve non-URL notes, 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:
|
For suggestions, it combines:
|
||||||
|
|
||||||
@@ -123,7 +137,7 @@ For suggestions, it combines:
|
|||||||
- the user's existing tag vocabulary from `fetchTags()`,
|
- the user's existing tag vocabulary from `fetchTags()`,
|
||||||
- backend AI tag suggestions through `/ShareExtension/TagSuggester.swift`.
|
- 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.
|
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. The same parser is used by in-app Add Bookmark text import.
|
||||||
|
|
||||||
## Widget architecture and deep links
|
## Widget architecture and deep links
|
||||||
|
|
||||||
@@ -157,4 +171,4 @@ Widget links use custom URLs built by `URL.marksBookmark(_:)` and `URL.marksPodc
|
|||||||
|
|
||||||
## Recent history context
|
## 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.
|
Recent git history shows major investment in ingest and podcast UX: the latest change added local text/PDF/file sources, richer URL extraction for share/add flows, source Spotlight indexing, and source-backed podcast generation. Earlier podcast and AI changes added played/unplayed queues, background podcast generation, share-sheet audio requests, podcast library/index changes, player improvements, 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.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ xcodegen generate
|
|||||||
Common target-membership traps:
|
Common target-membership traps:
|
||||||
|
|
||||||
- The main app includes `/MarksWidget/WidgetDataStore.swift` directly so it can write widget JSON.
|
- 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 share extension includes selected shared files from `/Marks/Models` and `/Marks/Services`, including `IngestPayload.swift` for URL extraction; 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.
|
- The app, widget, and share extension all depend on matching app-group entitlements.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -46,6 +46,8 @@ Covered today:
|
|||||||
- `IntentRouter` open/search setters.
|
- `IntentRouter` open/search setters.
|
||||||
- Direct no-network `perform()` tests for Search and Open intents.
|
- Direct no-network `perform()` tests for Search and Open intents.
|
||||||
- App Shortcuts registration count.
|
- App Shortcuts registration count.
|
||||||
|
- `IngestPayloadParser` URL extraction from direct URLs, plain text, HTML-ish text, and notes.
|
||||||
|
- `IngestedSourceStore` save/load behavior plus plain-text file import into a temporary root.
|
||||||
|
|
||||||
Not currently covered by repository tests:
|
Not currently covered by repository tests:
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ Not currently covered by repository tests:
|
|||||||
- Widget timeline rendering/deep links.
|
- Widget timeline rendering/deep links.
|
||||||
- Browser reading progress/reader mode.
|
- Browser reading progress/reader mode.
|
||||||
- Podcast generation, playback, queue, played/unplayed, and background audio behavior.
|
- Podcast generation, playback, queue, played/unplayed, and background audio behavior.
|
||||||
|
- Sources tab UI, QuickLook presentation, PDF extraction edge cases, and source-backed podcast backend behavior.
|
||||||
|
|
||||||
Suggested test command:
|
Suggested test command:
|
||||||
|
|
||||||
@@ -88,17 +91,26 @@ AI changes:
|
|||||||
|
|
||||||
Share extension changes:
|
Share extension changes:
|
||||||
|
|
||||||
- Share a new URL and an already-saved URL.
|
- Share a new URL, an already-saved URL, and text/HTML/RTF content that contains a URL plus notes.
|
||||||
- Verify duplicate-aware update behavior.
|
- Verify duplicate-aware update behavior.
|
||||||
- Test linkding auto-tags, AI suggestion chips, tag parsing, notes, read-later, and Create Podcast.
|
- 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.
|
- Confirm the extension works when shared config is missing; current UI shows an "Open Marks first" style failure.
|
||||||
|
|
||||||
|
Source ingest changes:
|
||||||
|
|
||||||
|
- Import typed text with title/tags and confirm it appears in Sources search.
|
||||||
|
- Import a text file and a PDF through the file importer; confirm originals copy into Application Support and QuickLook opens imported files.
|
||||||
|
- Confirm PDF text extraction works for text PDFs and handles image-only PDFs gracefully.
|
||||||
|
- Delete a source and confirm metadata, copied file, and Spotlight entry are removed.
|
||||||
|
- Create a podcast from a source and confirm cached playback uses the same `marks-source://` entry.
|
||||||
|
|
||||||
Podcast changes:
|
Podcast changes:
|
||||||
|
|
||||||
- Generate from bookmark row and Browser view.
|
- Generate from bookmark row and Browser view.
|
||||||
- Start generation while another podcast is playing; confirm background generation does not interrupt playback.
|
- 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.
|
- 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.
|
- Share a URL with Create Podcast and confirm the main app drains and starts the queued request.
|
||||||
|
- Generate from a local text/PDF source and confirm backend support for `text` and `source_kind`.
|
||||||
|
|
||||||
Widget/deep-link changes:
|
Widget/deep-link changes:
|
||||||
|
|
||||||
@@ -136,6 +148,8 @@ App-group defaults/files (`group.com.magicive.marks`):
|
|||||||
Application Support files:
|
Application Support files:
|
||||||
|
|
||||||
- `bookmarks_cache.json` or `bookmarks_{host}.json` — cached bookmarks.
|
- `bookmarks_cache.json` or `bookmarks_{host}.json` — cached bookmarks.
|
||||||
|
- `Sources/sources.json` — local source metadata.
|
||||||
|
- `Sources/files/{uuid}.{ext}` — copied originals for imported PDFs/files.
|
||||||
- `podcasts/{hash}.mp3` — generated podcast audio.
|
- `podcasts/{hash}.mp3` — generated podcast audio.
|
||||||
- `podcasts/index.json` — podcast metadata and played state.
|
- `podcasts/index.json` — podcast metadata and played state.
|
||||||
|
|
||||||
@@ -148,7 +162,8 @@ Application Support files:
|
|||||||
- `MarksAuth` stores backend JWT/device state in `UserDefaults`, not Keychain.
|
- `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.
|
- 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.
|
- 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.
|
- Source-backed podcast generation sends extracted text to the Marks backend; if the backend only accepts URL fetches, local source podcasts will fail even though bookmark podcasts still work.
|
||||||
|
- Recent git history shows source ingest, podcast, and on-device AI features were added across many files; avoid narrow changes that ignore cross-target data flow.
|
||||||
|
|
||||||
## Existing design guidance
|
## Existing design guidance
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
# Marks OpenWiki Quickstart
|
# 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.
|
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, local source ingest, and podcast generation/playback.
|
||||||
|
|
||||||
Start here when changing the repository, then follow the page links below for the area you are touching.
|
Start here when changing the repository, then follow the page links below for the area you are touching.
|
||||||
|
|
||||||
## What this repository contains
|
## What this repository contains
|
||||||
|
|
||||||
- **Main iOS app** in `/Marks` with SwiftUI tabs for bookmarks, tags, podcasts, and search. The entrypoint is `/Marks/MarksApp.swift`.
|
- **Main iOS app** in `/Marks` with SwiftUI tabs for bookmarks, tags, sources, 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`.
|
- **Models** in `/Marks/Models`, centered on `Bookmark`, linkding response/create/update payloads, tags, smart collections, `ServerConfig`, URL ingest payloads, and locally ingested sources.
|
||||||
- **Services** in `/Marks/Services` for linkding networking, AI/backend calls, auth, on-device bookmark Q&A, Spotlight indexing, widget/podcast data, analytics, and logging.
|
- **Services** in `/Marks/Services` for linkding networking, AI/backend calls, auth, on-device bookmark Q&A, Spotlight indexing, source storage, widget/podcast data, analytics, and logging.
|
||||||
- **Views** in `/Marks/Views` for the primary product surfaces. `BookmarksViewModel` is the central app state object.
|
- **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.
|
- **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.
|
- **Share extension** in `/ShareExtension` for extracting URLs from shared URLs, text, HTML, and RTF, saving to linkding, and queueing podcast generation.
|
||||||
- **Widget extension** in `/MarksWidget` for recent bookmarks, random bookmark, and recent podcast widgets.
|
- **Widget extension** in `/MarksWidget` for recent bookmarks, random bookmark, and recent podcast widgets.
|
||||||
- **Tests** in `/MarksTests`, currently focused on the App Intents layer.
|
- **Tests** in `/MarksTests`, currently focused on App Intents plus ingest parser/source-store coverage.
|
||||||
- **XcodeGen project spec** in `/project.yml`; `Marks.xcodeproj` is generated/checked in but `project.yml` is the project definition to inspect first.
|
- **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.
|
- **Design notes** in `/docs/design-wwdc26.md` for iOS 26/Liquid Glass, search, widgets, and row-design guidance.
|
||||||
|
|
||||||
@@ -46,9 +46,11 @@ Adjust the simulator name to what is installed locally.
|
|||||||
|
|
||||||
## Runtime architecture in one minute
|
## 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.
|
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, Sources, 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.
|
`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. `MainContainer` separately owns `IngestedSourceLibrary` for the Sources tab.
|
||||||
|
|
||||||
|
Local sources are stored in Application Support under `Sources/`: `sources.json` stores metadata, `Sources/files/` stores imported files, PDF text is extracted with PDFKit, and sources are indexed into Spotlight through `SourceSpotlightIndexer`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ Extensions and widgets use the shared app group `group.com.magicive.marks`. `Ser
|
|||||||
|
|
||||||
- **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.
|
- **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.
|
- **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.
|
- **Apple on-device intelligence and Spotlight** power the in-app Ask surface via `FoundationModels`, `CoreSpotlight`, and the `BookmarkSearchTool` retrieval path. Spotlight is also used for locally ingested sources.
|
||||||
- **WidgetKit, App Intents, AVFoundation, and MediaPlayer** support widgets, Siri/Shortcuts integration, podcast playback, background audio, now-playing metadata, and remote controls.
|
- **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.
|
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.
|
||||||
@@ -68,14 +70,16 @@ Do not document or expose secret values. `/Marks/MarksApp.swift` currently conta
|
|||||||
- 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.
|
- 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.
|
- `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.
|
- 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.
|
- Local source podcasts use `marks-source://{uuid}` pseudo-URLs plus extracted text sent to `ClaudeService.generatePodcast(...)`. The backend must accept `text` and `source_kind` for non-web podcast generation.
|
||||||
|
- Existing automated tests cover App Intents plus focused ingest parser/source-store behavior. For UI, networking, sync, share extension, widgets, and podcasts, add tests where feasible and perform manual verification.
|
||||||
|
|
||||||
## Where to start for common changes
|
## 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`.
|
- 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`.
|
- 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`.
|
- 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`.
|
- Source ingest and local documents: `/Marks/Views/SourcesView.swift`, `/Marks/Models/IngestedSource.swift`, `/Marks/Services/IngestedSourceStore.swift`, `/Marks/Services/SourceSpotlightIndexer.swift`.
|
||||||
- Share extension: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, plus shared files declared in `/project.yml`.
|
- Podcast generation/playback: `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/SourcesView.swift`.
|
||||||
|
- Share extension and text/link extraction: `/Marks/Models/IngestPayload.swift`, `/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`.
|
- Widgets/deep links: `/MarksWidget/WidgetDataStore.swift`, `/MarksWidget/*Widget.swift`, `/Marks/MarksApp.swift`.
|
||||||
- App Intents/Siri/Shortcuts: `/Marks/Intents/*`, `/MarksTests/AppIntentsTests.swift`.
|
- App Intents/Siri/Shortcuts: `/Marks/Intents/*`, `/MarksTests/AppIntentsTests.swift`.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Change guidance:
|
|||||||
|
|
||||||
Primary sources: `/Marks/Views/AddBookmarkView.swift`, `/Marks/Views/EditBookmarkView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Models/Bookmark.swift`.
|
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 Add view collects URL, optional title, description/notes, and comma-separated tags, normalizes/validates the URL, and calls `BookmarksViewModel.addBookmark(url:title:description:tags:)`. It also has a paste/import text path that uses `IngestPayloadParser` to extract the first `http`/`https` URL from plain text and preserve surrounding notes as the bookmark description. 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.
|
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.
|
||||||
|
|
||||||
@@ -80,9 +80,9 @@ Change guidance:
|
|||||||
|
|
||||||
## Share extension save flow
|
## Share extension save flow
|
||||||
|
|
||||||
Primary sources: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, `/Marks/Services/PodcastRequests.swift`, `/project.yml`.
|
Primary sources: `/Marks/Models/IngestPayload.swift`, `/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:
|
The share extension scans URL, HTML, RTF, plain-text, and text attachments, then `IngestPayloadParser` extracts the first bookmarkable web URL. For non-URL text payloads it also preserves cleaned surrounding text as notes. On load it:
|
||||||
|
|
||||||
1. reads linkding config from app-group defaults,
|
1. reads linkding config from app-group defaults,
|
||||||
2. calls `LinkdingAPI.checkBookmark(url:)`,
|
2. calls `LinkdingAPI.checkBookmark(url:)`,
|
||||||
@@ -96,13 +96,35 @@ Change guidance:
|
|||||||
|
|
||||||
- The extension is short-lived. Keep long-running work in the main app.
|
- 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.
|
- The extension depends on shared source files listed in `/project.yml`; adding service dependencies may require target membership updates.
|
||||||
|
- `IngestPayloadParser` intentionally ignores non-HTTP(S) URLs and caps preserved notes at 1,800 characters.
|
||||||
- `ShareExtension/Info.plist` currently uses a broad activation rule. Narrowing supported payloads is a product decision.
|
- `ShareExtension/Info.plist` currently uses a broad activation rule. Narrowing supported payloads is a product decision.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
Primary sources: `/Marks/Views/SourcesView.swift`, `/Marks/Models/IngestedSource.swift`, `/Marks/Services/IngestedSourceStore.swift`, `/Marks/Services/SourceSpotlightIndexer.swift`.
|
||||||
|
|
||||||
|
The Sources tab stores non-web material in Marks. Users can:
|
||||||
|
|
||||||
|
- import typed/pasted text with an optional title and tags,
|
||||||
|
- import PDFs, plain text files, generic text/data files through the document picker,
|
||||||
|
- search across source title, body text, tags, and original filename,
|
||||||
|
- open imported originals through QuickLook,
|
||||||
|
- delete sources and copied files,
|
||||||
|
- create or play a podcast from extractable source text.
|
||||||
|
|
||||||
|
`IngestedSourceLibrary.load()` reads `Sources/sources.json` from Application Support and indexes sources into Spotlight. Text imports write metadata/body text directly. File imports copy the selected security-scoped file into `Sources/files/`; PDFs extract text with PDFKit, while other files attempt UTF-8/ASCII extraction.
|
||||||
|
|
||||||
|
Change guidance:
|
||||||
|
|
||||||
|
- Empty or image-only PDFs may import with no extractable text; podcast actions are disabled when `podcastText` is empty.
|
||||||
|
- Source podcasts use `marks-source://{uuid}` as the cache/index URL and send extracted text plus `source_kind` to the backend.
|
||||||
|
- If changing source storage, update deletion, Spotlight removal, QuickLook file lookup, and source podcast cache behavior together.
|
||||||
|
|
||||||
## Podcasts
|
## 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`.
|
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:
|
Podcast generation converts an article URL, or a locally ingested source with extracted text, into audio through the Marks backend:
|
||||||
|
|
||||||
1. create generation job (`/v1/podcast/generate`),
|
1. create generation job (`/v1/podcast/generate`),
|
||||||
2. poll status every few seconds,
|
2. poll status every few seconds,
|
||||||
@@ -112,7 +134,7 @@ Podcast generation converts an article URL into audio through the Marks backend:
|
|||||||
6. reload `PodcastLibrary`,
|
6. reload `PodcastLibrary`,
|
||||||
7. write recent podcast metadata for widgets.
|
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`.
|
`BookmarksViewModel.playOrGeneratePodcast(...)` prevents interrupting current bookmark playback. If the player is idle, it starts foreground generation/playback and callers present the player. If busy, it enqueues background generation through `PodcastGenerationManager`. `SourcesView` follows the same foreground/background pattern for local sources, using `marks-source://{uuid}` plus `sourceText`/`sourceKind`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -121,6 +143,7 @@ Change guidance:
|
|||||||
- The main app plist enables background audio and the custom `marks` URL scheme.
|
- 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.
|
- 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.
|
- Share-extension queued podcast requests are drained by `BookmarksViewModel.processPendingPodcastRequests()` on launch/foreground. Consider retry semantics before changing drain behavior.
|
||||||
|
- Source-backed podcast generation depends on backend support for the optional `text` and `source_kind` fields in `POST /v1/podcast/generate`.
|
||||||
|
|
||||||
## Widgets and deep links
|
## Widgets and deep links
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user