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:
@@ -32,18 +32,46 @@ struct VoteMoodIntent: AppIntent {
|
||||
let mood = Mood(rawValue: moodValue) ?? .average
|
||||
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
|
||||
|
||||
// Add mood entry
|
||||
// Widget uses simplified mood logging since it can't access HealthKitManager/TipsManager
|
||||
// Full side effects (HealthKit sync, TipKit) will run when main app opens via MoodLogger
|
||||
DataController.shared.add(mood: mood, forDate: votingDate, entryType: .widget)
|
||||
|
||||
// Store last voted date
|
||||
let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate))
|
||||
GroupUserDefaults.groupDefaults.set(dateString, forKey: UserDefaultsStore.Keys.lastVotedDate.rawValue)
|
||||
|
||||
// Update Live Activity
|
||||
let streak = calculateCurrentStreak()
|
||||
LiveActivityManager.shared.updateActivity(streak: streak, mood: mood)
|
||||
LiveActivityScheduler.shared.scheduleForNextDay()
|
||||
|
||||
// Reload widget timeline
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "FeelsVoteWidget")
|
||||
|
||||
return .result()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func calculateCurrentStreak() -> 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: - Vote Widget Provider
|
||||
@@ -75,13 +103,41 @@ struct VoteWidgetProvider: TimelineProvider {
|
||||
Task { @MainActor in
|
||||
let entry = createEntry()
|
||||
|
||||
// Refresh at midnight
|
||||
let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
||||
let timeline = Timeline(entries: [entry], policy: .after(midnight))
|
||||
// Calculate next refresh time
|
||||
let nextRefresh = calculateNextRefreshDate()
|
||||
let timeline = Timeline(entries: [entry], policy: .after(nextRefresh))
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate when the widget should next refresh
|
||||
/// Refreshes at: rating time (to show voting view) and midnight (for new day)
|
||||
private func calculateNextRefreshDate() -> Date {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
|
||||
// Get the rating time from onboarding data
|
||||
let onboardingData = UserDefaultsStore.getOnboarding()
|
||||
let ratingTimeComponents = calendar.dateComponents([.hour, .minute], from: onboardingData.date)
|
||||
|
||||
// Create today's rating time
|
||||
var todayRatingComponents = calendar.dateComponents([.year, .month, .day], from: now)
|
||||
todayRatingComponents.hour = ratingTimeComponents.hour
|
||||
todayRatingComponents.minute = ratingTimeComponents.minute
|
||||
let todayRatingTime = calendar.date(from: todayRatingComponents) ?? now
|
||||
|
||||
// Tomorrow's midnight
|
||||
let midnight = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: now)!)
|
||||
|
||||
// If we haven't passed today's rating time, refresh at rating time
|
||||
if now < todayRatingTime {
|
||||
return todayRatingTime
|
||||
}
|
||||
|
||||
// Otherwise refresh at midnight
|
||||
return midnight
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createEntry() -> VoteWidgetEntry {
|
||||
let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
||||
|
||||
Reference in New Issue
Block a user