Files
Reflect/Shared/AppShortcuts.swift
Trey t 440b04159e Add Apple platform features and UX improvements
- Add HealthKit State of Mind sync for mood entries
- Add Live Activity with streak display and rating time window
- Add App Shortcuts/Siri integration for voice mood logging
- Add TipKit hints for feature discovery
- Add centralized MoodLogger for consistent side effects
- Add reminder time setting in Settings with time picker
- Fix duplicate notifications when changing reminder time
- Fix Live Activity streak showing 0 when not yet rated today
- Fix slow tap response in entry detail mood selection
- Update widget timeline to refresh at rating time
- Sync widgets when reminder time changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 17:21:55 -06:00

218 lines
7.0 KiB
Swift

//
// AppShortcuts.swift
// Feels
//
// 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 Feels")
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 Feels")
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 FeelsShortcuts: 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"
)
}
}