Saved Articles tauchen im iOS-System-Spotlight auf. Siri kennt zwei
Phrasen — App öffnen + URL speichern. Beide Pfade nutzen das in π-5
gebaute pageta:// scheme als Re-Entry-Point.
- Sources/Core/SpotlightIndex.swift:
- CSSearchableItem mit title, contentDescription (excerpt fallback
siteName), contentURL pageta://article/{id}, keywords-Kaskade
(Site, Author, Status-Label).
- domainIdentifier "ev.mana.pageta.articles" — clearAll beim
Logout entfernt Pageta-Items chirurgisch ohne andere Apps zu
treffen.
- indexArticles() + clearAll() — beide soft-fail (Spotlight ist
sekundär).
- LeseListeTabView.loadArticles: nach Server-Fetch sync zum
Spotlight-Index. User sucht meistens das, was er gerade gespeichert
hat.
- AccountTabView Logout: erst SpotlightIndex.clearAll, dann
auth.signOut — sonst leakt der Index zwischen Accounts auf dem
selben Device.
- Sources/Core/AppShortcuts.swift:
- OpenFeedIntent: "Hey Siri, Pageta öffnen" → App im Feed-Tab.
- SaveURLIntent: "Speichere [URL] in Pageta" → enqueued via
PendingShareStore (headless, kein App-Open).
- PagetaShortcuts AppShortcutsProvider mit 5 DE-Phrasen + Icons.
Spotlight-Tap routet via contentURL → onOpenURL → PagetaRoute.parse
(π-5) → Reader-Sheet. Kein extra NSUserActivityTypes-Setup nötig.
1 neuer Test (SearchableItem-Field-Coverage), 26/26 grün,
iOS+macOS Build green.
Phase 2 jetzt 4/4 (π-5..π-8). Restlich nur π-9 macOS-Polish +
π-10 ASC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
Swift
77 lines
2.7 KiB
Swift
import Foundation
|
|
#if canImport(CoreSpotlight)
|
|
import CoreSpotlight
|
|
#endif
|
|
#if canImport(UniformTypeIdentifiers)
|
|
import UniformTypeIdentifiers
|
|
#endif
|
|
|
|
/// Schreibt saved Articles in den iOS-Spotlight-Index, damit der User
|
|
/// sie in der system-weiten Suche findet (Pull-Down auf Home-Screen).
|
|
///
|
|
/// **Indexing-Modell**: pro Article eine `CSSearchableItem` unter dem
|
|
/// Domain `ev.mana.pageta.articles`. Identifier ist die articleId,
|
|
/// damit Re-Index dieselbe Stelle überschreibt. ContentURL ist der
|
|
/// `pageta://article/{id}` Deep-Link — beim Tap auf das Spotlight-
|
|
/// Result öffnet iOS die App mit `.onContinueUserActivity`, dann
|
|
/// greift unser PagetaRoute-Parser aus π-5.
|
|
///
|
|
/// **Lifecycle**: LeseListeTabView triggert `indexArticles` nach
|
|
/// jedem erfolgreichen `/articles?status=...`-Fetch. AccountTabView
|
|
/// triggert `clearAll` beim Logout (sonst leakt der Index zwischen
|
|
/// Accounts).
|
|
struct SpotlightIndex: Sendable {
|
|
static let domainIdentifier = "ev.mana.pageta.articles"
|
|
|
|
/// Schreibt die übergebenen Articles in den default-Index.
|
|
/// Soft-Fail bei Errors — Spotlight ist sekundär.
|
|
@discardableResult
|
|
func indexArticles(_ articles: [Article]) async -> Int {
|
|
#if canImport(CoreSpotlight)
|
|
let items = articles.map(Self.makeSearchableItem(_:))
|
|
do {
|
|
try await CSSearchableIndex.default().indexSearchableItems(items)
|
|
return items.count
|
|
} catch {
|
|
return 0
|
|
}
|
|
#else
|
|
return 0
|
|
#endif
|
|
}
|
|
|
|
/// Entfernt alle Pageta-Items aus dem Index. Beim Logout pflicht.
|
|
func clearAll() async {
|
|
#if canImport(CoreSpotlight)
|
|
try? await CSSearchableIndex.default()
|
|
.deleteSearchableItems(withDomainIdentifiers: [Self.domainIdentifier])
|
|
#endif
|
|
}
|
|
|
|
#if canImport(CoreSpotlight)
|
|
static func makeSearchableItem(_ article: Article) -> CSSearchableItem {
|
|
let attrs = CSSearchableItemAttributeSet(contentType: .text)
|
|
attrs.title = article.title
|
|
attrs.contentDescription = article.excerpt
|
|
?? article.siteName
|
|
?? ""
|
|
attrs.contentURL = URL(string: "pageta://article/\(article.id)")
|
|
attrs.identifier = article.id
|
|
attrs.domainIdentifier = SpotlightIndex.domainIdentifier
|
|
// Keyword-Kaskade: Site, Author, Status. Spotlight gewichtet
|
|
// keywords-Matches höher als Body. Hilft bei kurzen Suchen
|
|
// wie "spektrum" oder "ungelesen".
|
|
var keywords: [String] = []
|
|
if let site = article.siteName { keywords.append(site) }
|
|
if let author = article.author { keywords.append(author) }
|
|
keywords.append(article.status.label)
|
|
keywords.append("pageta")
|
|
attrs.keywords = keywords
|
|
return CSSearchableItem(
|
|
uniqueIdentifier: article.id,
|
|
domainIdentifier: SpotlightIndex.domainIdentifier,
|
|
attributeSet: attrs
|
|
)
|
|
}
|
|
#endif
|
|
}
|