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>
94 lines
2.9 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|