Library design: cards + redesigned list on the bookmarks screen, tag picker sheet #9

Merged
admin merged 11 commits from feat/library-browse-prototype into master 2026-07-30 04:03:03 +00:00
4 changed files with 59 additions and 5 deletions
Showing only changes of commit 69556afc08 - Show all commits
+4
View File
@@ -25,6 +25,7 @@
457FCE503CCA82C5F27C6C90 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA428181B35885F7D9F4D55 /* Bookmark.swift */; };
50F3BED92EBA34F863C9F8A0 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7510EB352E624C7C9656EA33 /* LibraryView.swift */; };
55CDFFAB5530D08861F85363 /* LibraryGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF2B010DADCCFBC282D37F0 /* LibraryGridView.swift */; };
59E46C9653C9033F5BB1CEC7 /* SpotlightRetrievalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */; };
5D86F3F0F603B248776916C7 /* BookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFB5EFC9764B22A2622EA4A /* BookmarksViewModel.swift */; };
5ED7F0AB24549BA01757A39C /* PodcastPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4EB8C63735A267B81030CB5 /* PodcastPlayerView.swift */; };
66D5D90A5FAF842BCA0FE72D /* PodcastRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27A97922BAEBDC9C5A7385C /* PodcastRequests.swift */; };
@@ -136,6 +137,7 @@
22E006A11D594BFC00A9C4B4 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
23F172EC9977CD5C51B228B9 /* MarksWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = MarksWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
240DBB87940F8D255A812EB2 /* BookmarkActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkActions.swift; sourceTree = "<group>"; };
2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightRetrievalTests.swift; sourceTree = "<group>"; };
41DDBB04346F3BF06DE233D2 /* SpotlightBookmarkSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightBookmarkSearch.swift; sourceTree = "<group>"; };
47CB3AAED5B64809B06A9650 /* RecentPodcastsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentPodcastsWidget.swift; sourceTree = "<group>"; };
49685B8F3FEC72E8CF75843E /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
@@ -244,6 +246,7 @@
isa = PBXGroup;
children = (
7623601C25E481DF58371F2A /* AppIntentsTests.swift */,
2526E22D8EC5453358F0FCF9 /* SpotlightRetrievalTests.swift */,
);
path = MarksTests;
sourceTree = "<group>";
@@ -528,6 +531,7 @@
buildActionMask = 2147483647;
files = (
FD656A44CEE8AE28B136AD85 /* AppIntentsTests.swift in Sources */,
59E46C9653C9033F5BB1CEC7 /* SpotlightRetrievalTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+12 -2
View File
@@ -17,7 +17,17 @@ struct BookmarkEntity: AppEntity, IndexedEntity {
@Property(title: "Title")
var title: String
@Property(title: "URL")
/// The explicit `indexingKey` is load-bearing. Left to itself App Intents
/// indexes this property under the attribute set's own `url` key, and
/// Spotlight's Cascade translator types that field as NSString so every
/// item failed with "Provided object for field url is of class NSURL,
/// expected class: NSString" and nothing reached the index. `contentURL`
/// is URL-typed there, and is the right field for "where this lives"
/// anyway. Routing it there keeps the property a URL for Shortcuts.
///
/// The failure is silent: translation happens after `indexAppEntities`
/// returns, so indexing logs success either way.
@Property(title: "URL", indexingKey: \.contentURL)
var url: URL
@Property(title: "Website")
@@ -53,7 +63,7 @@ struct BookmarkEntity: AppEntity, IndexedEntity {
attrs.title = title
attrs.contentDescription = details.isEmpty ? summary : details
attrs.keywords = tags
attrs.url = url
attrs.contentURL = url
return attrs
}
}
+3 -3
View File
@@ -24,7 +24,7 @@ enum SpotlightBookmarkSearch {
Log.spotlight.debug("Search query=\(rawQuery, privacy: .public) predicate=\(queryString, privacy: .public)")
let context = CSSearchQueryContext()
context.fetchAttributes = ["title", "contentDescription", "keywords", "url"]
context.fetchAttributes = ["title", "contentDescription", "keywords", "contentURL"]
let query = CSSearchQuery(queryString: queryString, queryContext: context)
var out: [RetrievedBookmark] = []
@@ -33,9 +33,9 @@ enum SpotlightBookmarkSearch {
let a = result.item.attributeSet
out.append(RetrievedBookmark(
title: a.title ?? "Untitled",
host: a.url?.host() ?? "",
host: a.contentURL?.host() ?? "",
description: a.contentDescription ?? "",
url: a.url?.absoluteString ?? ""
url: a.contentURL?.absoluteString ?? ""
))
if out.count >= limit { break }
}
+40
View File
@@ -0,0 +1,40 @@
import Testing
import CoreSpotlight
@testable import Marks
/// End-to-end guard for the whole Spotlight path: index a bookmark, then
/// retrieve it the way the on-device assistant does.
///
/// Worth keeping. The bug this was written for made every item fail to
/// translate into the index while `indexAppEntities` still reported success,
/// so nothing short of a round trip would have caught it.
struct SpotlightRetrievalTests {
@Test func indexedBookmarkIsRetrievable() async throws {
let b = Bookmark(
id: 999_001,
url: "https://github.com/example/zqxjkltest",
title: "Zqxjkltest concurrency notes",
description: "A distinctive probe document about zqxjkltest.",
tagNames: ["zqxjkltest"],
dateAdded: Date(), dateModified: Date(),
isArchived: false, unread: false, shared: false,
websiteTitle: nil, websiteDescription: nil,
faviconUrl: nil, previewImageUrl: nil,
aiSummary: nil, aiTags: nil
)
await SpotlightIndexer.index([b])
var hits: [RetrievedBookmark] = []
for _ in 0..<20 {
try? await Task.sleep(for: .milliseconds(700))
hits = await SpotlightBookmarkSearch.run(query: "zqxjkltest", limit: 5)
if !hits.isEmpty { break }
}
await SpotlightIndexer.remove(ids: [999_001])
#expect(!hits.isEmpty, "Spotlight returned no hits for an indexed bookmark")
let hit = try #require(hits.first)
#expect(hit.title.contains("Zqxjkltest"))
#expect(hit.url == "https://github.com/example/zqxjkltest",
"url did not survive the round trip: \(hit.url)")
#expect(hit.host == "github.com", "host was \(hit.host)")
}
}