pageta-native/Sources/Core/SpotlightIndex.swift
Till JS 75597006cc π-8: Spotlight-Index + AppShortcuts (Siri)
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>
2026-05-18 17:20:50 +02:00

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
}