// // WidgetProviders.swift // ReflectWidget // // Timeline providers for widget data // import WidgetKit import SwiftUI import Intents // MARK: - Timeline Creator struct TimeLineCreator { @MainActor static func createViews(daysBack: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let dates = Array(0...daysBack).map({ Calendar.current.date(byAdding: .day, value: -$0, to: latestDayToShow)! }) // Use WidgetDataProvider for isolated widget data access let dataProvider = WidgetDataProvider.shared for date in dates { let dayStart = Calendar.current.startOfDay(for: date) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() if let todayEntry = dataProvider.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood), graphic: moodImages.icon(forMood: todayEntry.mood), date: dayStart, color: moodTint.color(forMood: todayEntry.mood), secondaryColor: moodTint.secondary(forMood: todayEntry.mood), mood: todayEntry.mood)) } else { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing), graphic: moodImages.icon(forMood: .missing), date: dayStart, color: moodTint.color(forMood: .missing), secondaryColor: moodTint.secondary(forMood: .missing), mood: .missing)) } } timeLineView = timeLineView.sorted(by: { $0.date > $1.date }) return timeLineView } /// Creates sample preview data for widget picker - shows what widget looks like with mood data static func createSampleViews(count: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() let sampleMoods: [Mood] = [.great, .good, .average, .good, .great, .average, .bad, .good, .great, .good, .average] let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() for i in 0.. SimpleEntry { return SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: TimeLineCreator.createSampleViews(count: 10)) } @MainActor func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { // Use sample data for widget picker preview, real data otherwise let timeLineViews: [WatchTimelineView] if context.isPreview { timeLineViews = TimeLineCreator.createSampleViews(count: 10) } else { timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) } let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus() let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: timeLineViews, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, promptText: promptText) completion(entry) } @MainActor func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { let (hasSubscription, hasVotedToday, promptText) = checkSubscriptionAndVoteStatus() let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!, configuration: ConfigurationIntent(), timeLineViews: nil, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, promptText: promptText) let midNightEntry = SimpleEntry(date: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())!, configuration: ConfigurationIntent(), timeLineViews: nil, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, promptText: promptText) let date = Calendar.current.date(byAdding: .second, value: 10, to: Date())! let timeline = Timeline(entries: [entry, midNightEntry], policy: .after(date)) completion(timeline) } @MainActor private func checkSubscriptionAndVoteStatus() -> (hasSubscription: Bool, hasVotedToday: Bool, promptText: String) { let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) 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) ?? dayStart // Use WidgetDataProvider for isolated widget data access let todayEntry = WidgetDataProvider.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body return (hasSubscription, hasVotedToday, promptText) } } // MARK: - Vote Widget Provider struct VoteWidgetProvider: TimelineProvider { func placeholder(in context: Context) -> VoteWidgetEntry { // Show sample "already voted" state for widget picker preview let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]) let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body return VoteWidgetEntry(date: Date(), votingDate: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText) } func getSnapshot(in context: Context, completion: @escaping (VoteWidgetEntry) -> Void) { // Show sample data for widget picker preview if context.isPreview { let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]) let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body let entry = VoteWidgetEntry(date: Date(), votingDate: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats, promptText: promptText) completion(entry) return } Task { @MainActor in let entry = createEntry() completion(entry) } } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { Task { @MainActor in let entry = createEntry() // 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) // Use WidgetDataProvider for isolated read-only data access let dataProvider = WidgetDataProvider.shared 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)! // Check if user has voted today let todayEntry = dataProvider.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder // Get today's mood if voted let todaysMood: Mood? = hasVotedToday ? todayEntry?.mood : nil // Get stats for display after voting var stats: MoodStats? = nil if hasVotedToday { let allEntries = dataProvider.getData( startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: [] ) let validEntries = allEntries.filter { $0.mood != Mood.missing && $0.mood != Mood.placeholder } let totalCount = validEntries.count if totalCount > 0 { var moodCounts: [Mood: Int] = [:] for entry in validEntries { moodCounts[entry.mood, default: 0] += 1 } stats = MoodStats(totalEntries: totalCount, moodCounts: moodCounts) } } // Get random prompt text for voting view let promptText = UserDefaultsStore.personalityPackable().randomPushNotificationStrings().body return VoteWidgetEntry( date: Date(), votingDate: votingDate, hasSubscription: hasSubscription, hasVotedToday: hasVotedToday, todaysMood: todaysMood, stats: stats, promptText: promptText ) } }