// // AppShortcuts.swift // Reflect // // App Intents and Siri Shortcuts for voice-activated mood logging // import AppIntents import SwiftUI // MARK: - Mood Entity for App Intents struct MoodEntity: AppEntity { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mood") static var defaultQuery = MoodEntityQuery() var id: Int var name: String var mood: Mood var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") } static let allMoods: [MoodEntity] = [ MoodEntity(id: 0, name: "Horrible", mood: .horrible), MoodEntity(id: 1, name: "Bad", mood: .bad), MoodEntity(id: 2, name: "Average", mood: .average), MoodEntity(id: 3, name: "Good", mood: .good), MoodEntity(id: 4, name: "Great", mood: .great) ] } struct MoodEntityQuery: EntityQuery { func entities(for identifiers: [Int]) async throws -> [MoodEntity] { MoodEntity.allMoods.filter { identifiers.contains($0.id) } } func suggestedEntities() async throws -> [MoodEntity] { MoodEntity.allMoods } func defaultResult() async -> MoodEntity? { MoodEntity.allMoods.first { $0.mood == .average } } } // MARK: - Log Mood Intent struct LogMoodIntent: AppIntent { static var title: LocalizedStringResource = "Log Mood" static var description = IntentDescription("Record your mood for today in Reflect") static var openAppWhenRun: Bool = false @Parameter(title: "Mood") var moodEntity: MoodEntity static var parameterSummary: some ParameterSummary { Summary("Log mood as \(\.$moodEntity)") } @MainActor func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView { let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) // Use centralized mood logger MoodLogger.shared.logMood(moodEntity.mood, for: votingDate, entryType: .siri) let moodTint = UserDefaultsStore.moodTintable() let color = moodTint.color(forMood: moodEntity.mood) return .result( dialog: "Got it! Logged \(moodEntity.name) for today.", view: MoodLoggedSnippetView(moodName: moodEntity.name, color: color) ) } } // MARK: - Check Today's Mood Intent struct CheckTodaysMoodIntent: AppIntent { static var title: LocalizedStringResource = "Check Today's Mood" static var description = IntentDescription("See what mood you logged today in Reflect") static var openAppWhenRun: Bool = false @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let dayStart = Calendar.current.startOfDay(for: votingDate) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! let todayEntry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first if let entry = todayEntry, entry.mood != .missing && entry.mood != .placeholder { return .result(dialog: "Today you logged feeling \(entry.mood.widgetDisplayName).") } else { return .result(dialog: "You haven't logged your mood today yet. Would you like to log it now?") } } } // MARK: - Get Mood Streak Intent struct GetMoodStreakIntent: AppIntent { static var title: LocalizedStringResource = "Get Mood Streak" static var description = IntentDescription("Check your current mood logging streak") static var openAppWhenRun: Bool = false @MainActor func perform() async throws -> some IntentResult & ProvidesDialog { let streak = calculateStreak() if streak == 0 { return .result(dialog: "You don't have a streak yet. Log your mood today to start one!") } else if streak == 1 { return .result(dialog: "You have a 1 day streak. Keep it going!") } else { return .result(dialog: "Amazing! You have a \(streak) day streak. Keep it up!") } } @MainActor private func calculateStreak() -> Int { var streak = 0 var checkDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) while true { let dayStart = Calendar.current.startOfDay(for: checkDate) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! let entry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first if let entry = entry, entry.mood != .missing && entry.mood != .placeholder { streak += 1 checkDate = Calendar.current.date(byAdding: .day, value: -1, to: checkDate)! } else { break } } return streak } } // MARK: - Snippet View for Mood Logged struct MoodLoggedSnippetView: View { let moodName: String let color: Color var body: some View { HStack(spacing: 12) { Circle() .fill(color) .frame(width: 44, height: 44) .overlay { Image(systemName: "checkmark") .font(.title2.bold()) .foregroundColor(.white) } VStack(alignment: .leading, spacing: 2) { Text("Mood Logged") .font(.headline) Text(moodName) .font(.subheadline) .foregroundColor(color) } Spacer() } .padding() } } // MARK: - App Shortcuts Provider struct ReflectShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: LogMoodIntent(), phrases: [ "Log my mood in \(.applicationName)", "Log mood as \(\.$moodEntity) in \(.applicationName)", "Record my mood in \(.applicationName)", "I'm feeling \(\.$moodEntity) in \(.applicationName)", "Track my mood in \(.applicationName)" ], shortTitle: "Log Mood", systemImageName: "face.smiling" ) AppShortcut( intent: CheckTodaysMoodIntent(), phrases: [ "What's my mood today in \(.applicationName)", "Check today's mood in \(.applicationName)", "How am I feeling in \(.applicationName)" ], shortTitle: "Today's Mood", systemImageName: "calendar" ) AppShortcut( intent: GetMoodStreakIntent(), phrases: [ "What's my mood streak in \(.applicationName)", "Check my streak in \(.applicationName)", "How many days in a row in \(.applicationName)" ], shortTitle: "Mood Streak", systemImageName: "flame" ) } }