Consolidate extension data providers and add side effects catch-up

- Create unified ExtensionDataProvider for Widget and Watch targets
- Remove duplicate WatchDataProvider and WatchConnectivityManager from Watch App
- Add side effects catch-up mechanism in MoodLogger for widget votes
- Process pending side effects on app launch and midnight background task
- Reduce ~450 lines of duplicated code across targets

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-21 22:50:23 -06:00
parent 83aad66f26
commit f7da61d6ca
12 changed files with 306 additions and 530 deletions

View File

@@ -0,0 +1,124 @@
//
// SharedMoodIntent.swift
// Feels
//
// Single VoteMoodIntent for all targets.
// Main app uses ForegroundContinuableIntent to run widget intents in app process.
//
// Add this file to ALL targets: Feels (iOS), FeelsWidgetExtension, Feels Watch App
//
import AppIntents
import SwiftUI
import SwiftData
import WidgetKit
import os.log
// MARK: - Vote Mood Intent
struct VoteMoodIntent: AppIntent {
static var title: LocalizedStringResource = "Vote Mood"
static var description = IntentDescription("Record your mood for today")
static var openAppWhenRun: Bool = false
@Parameter(title: "Mood")
var moodValue: Int
init() {
self.moodValue = 2
}
init(mood: Mood) {
self.moodValue = mood.rawValue
}
@MainActor
func perform() async throws -> some IntentResult {
let mood = Mood(rawValue: moodValue) ?? .average
#if os(watchOS)
// Watch: Send to iPhone via WatchConnectivity
let date = Date()
_ = WatchConnectivityManager.shared.sendMoodToPhone(mood: moodValue, date: date)
WidgetCenter.shared.reloadAllTimelines()
#elseif WIDGET_EXTENSION
// Widget: Save to shared container, main app handles side effects
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
WidgetMoodSaver.save(mood: mood, date: votingDate)
WidgetCenter.shared.reloadAllTimelines()
#else
// Main app: Full logging with all side effects
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
MoodLogger.shared.logMood(mood, for: votingDate, entryType: .widget)
let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate))
GroupUserDefaults.groupDefaults.set(dateString, forKey: UserDefaultsStore.Keys.lastVotedDate.rawValue)
#endif
return .result()
}
}
// MARK: - Main App: Run widget intents in app process
#if !WIDGET_EXTENSION && !os(watchOS)
extension VoteMoodIntent: ForegroundContinuableIntent {}
#endif
// MARK: - Widget: Save to shared container
#if WIDGET_EXTENSION
enum WidgetMoodSaver {
private static let logger = Logger(subsystem: "com.tt.ifeel.widget", category: "WidgetMoodSaver")
@MainActor
static func save(mood: Mood, date: Date) {
let schema = Schema([MoodEntryModel.self])
let appGroupID = Constants.currentGroupShareId
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else {
logger.error("App Group not available")
return
}
#if DEBUG
let storeURL = containerURL.appendingPathComponent("Feels-Debug.store")
#else
let storeURL = containerURL.appendingPathComponent("Feels.store")
#endif
do {
let config = ModelConfiguration(schema: schema, url: storeURL, cloudKitDatabase: .none)
let container = try ModelContainer(for: schema, configurations: [config])
let context = container.mainContext
// Delete existing entry for this date
let startDate = Calendar.current.startOfDay(for: date)
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
var descriptor = FetchDescriptor<MoodEntryModel>(
predicate: #Predicate { entry in
entry.forDate >= startDate && entry.forDate <= endDate
}
)
descriptor.fetchLimit = 1
if let existing = try? context.fetch(descriptor).first {
context.delete(existing)
try? context.save()
}
// Create new entry
let entry = MoodEntryModel(forDate: date, mood: mood, entryType: .widget)
context.insert(entry)
try context.save()
logger.info("Saved mood \(mood.rawValue) from widget")
} catch {
logger.error("Failed to save mood: \(error.localizedDescription)")
}
}
}
#endif