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>
This commit is contained in:
217
Shared/AppShortcuts.swift
Normal file
217
Shared/AppShortcuts.swift
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user