moodlit-native/Sources/Core/Search/SpotlightIndexer.swift
till 206fff422e μ-7.7: Spotlight-Indexing für Moods + Sequenzen
Presets + Custom-Moods + Sequenzen sind über CSSearchable im
Spotlight + Siri-Vorschlägen auffindbar. Tippen routet via
`uniqueIdentifier` (Format `mood:<id>` / `sequence:<id>`) in
RootView zurück → öffnet MoodPlayerView (Moods) oder springt in
Sequenzen-Tab.

Sources/Core/Search/SpotlightIndexer.swift mit zwei Domains
(`ev.mana.moodlit.moods`, `ev.mana.moodlit.sequences`). Domain-
basiertes `deleteSearchableItems` clearen beim Logout — kein
Eintrag des abgemeldeten Kontos bleibt im system-weiten Index.

MoodStore-Integration:
- `refreshSpotlightIndex()` läuft initial im RootView-`task`
  (Presets sofort findbar ohne Login) und nach jedem `loadAll`.
- `clearSpotlightIndex()` beim Auth-Wechsel signedOut.

RootView:
- `onContinueUserActivity(CSSearchableItemActionType)` parsed den
  uniqueIdentifier mit prefix-Check, setzt deepLinkMoodId oder
  selectedTab.
- `authStatusKey()`-Helper für Equatable-onChange auf AuthClient.Status
  (Cases mit assoziierten Werten sind nicht direkt Equatable).

xcodebuild iOS-Sim + macOS BUILD SUCCEEDED; 11/11 Unit-Tests grün.

ShareExt (Photo→Palette→Mood) wäre der natürliche moodlit-Use-
Case, ist aber 2-3h Vision/CoreImage-Arbeit für sich — deferred
auf eigenen Sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:39:42 +02:00

94 lines
2.9 KiB
Swift

import CoreSpotlight
import Foundation
import OSLog
import UniformTypeIdentifiers
/// CSSearchable-Index für Moodlit. Macht alle Presets + die User-
/// Custom-Moods + Sequenzen über Spotlight + Siri-Vorschläge
/// auffindbar. Tippen öffnet den Player über
/// `userInfo[CSSearchableItemActivityIdentifier]` = mood- oder
/// sequence-ID, die RootView konsumiert und in einen Deep-Link
/// `moodlit://play/<id>` (für Moods) bzw. einen Sequence-Open
/// übersetzt.
///
/// Pattern aus herbatrium-native. Sensible Daten (User-Mood-Namen)
/// laden wir bewusst in den system-weiten Index das ist die
/// gleiche Stufe wie iOS Safari-Suchverlauf, vertraglich akzeptabel.
/// Bei Logout cleared `clearAll`.
public enum SpotlightIndexer {
public static let domainMoods = "ev.mana.moodlit.moods"
public static let domainSequences = "ev.mana.moodlit.sequences"
private static let log = Logger(subsystem: AppConfig.bundleIdentifier, category: "spotlight")
/// Indiziert alle Presets, Custom-Moods und Sequenzen. Idempotent
/// CSSearchable-Index überschreibt bei gleicher uniqueIdentifier.
public static func index(moods: [Mood], sequences: [MoodSequence]) {
var items: [CSSearchableItem] = []
items.reserveCapacity(moods.count + sequences.count)
for mood in moods {
items.append(moodItem(mood))
}
for seq in sequences {
items.append(sequenceItem(seq))
}
CSSearchableIndex.default().indexSearchableItems(items) { error in
if let error {
log.error("indexSearchableItems failed: \(error.localizedDescription, privacy: .public)")
}
}
}
/// Löscht beide Domains. Beim Logout oder bei "Daten löschen".
public static func clearAll() {
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: [domainMoods, domainSequences]
) { error in
if let error {
log.error("deleteSearchableItems failed: \(error.localizedDescription, privacy: .public)")
}
}
}
// MARK: - Items
private static func moodItem(_ mood: Mood) -> CSSearchableItem {
let attrs = CSSearchableItemAttributeSet(contentType: UTType.content)
attrs.title = mood.name
attrs.contentDescription = "Mood · \(mood.animation.displayName)"
attrs.keywords = [
"Moodlit",
"Mood",
"Stimmung",
mood.name,
mood.animation.displayName,
mood.animation.rawValue,
]
attrs.identifier = mood.id
return CSSearchableItem(
uniqueIdentifier: "mood:\(mood.id)",
domainIdentifier: domainMoods,
attributeSet: attrs
)
}
private static func sequenceItem(_ seq: MoodSequence) -> CSSearchableItem {
let attrs = CSSearchableItemAttributeSet(contentType: UTType.content)
attrs.title = seq.name
attrs.contentDescription = "Sequenz · \(seq.moodIds.count) Moods · \(seq.durationSec)s je Mood"
attrs.keywords = [
"Moodlit",
"Sequenz",
"Sequence",
seq.name,
]
attrs.identifier = seq.id
return CSSearchableItem(
uniqueIdentifier: "sequence:\(seq.id)",
domainIdentifier: domainSequences,
attributeSet: attrs
)
}
}