import WidgetKit import SwiftUI import SwiftData import AppIntents struct AndromidaWidgetProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> WidgetEntry { WidgetEntry( date: Date(), configuration: ConfigurationAppIntent(), completionRate: 0.75, currentStreak: 5, nextHabits: [ HabitEntry(id: UUID(), title: "Morning Meditation", symbolName: "figure.mind.and.body", ritualTitle: "Mindfulness", isCompleted: false), HabitEntry(id: UUID(), title: "Drink Water", symbolName: "drop.fill", ritualTitle: "Health", isCompleted: true) ], weeklyTrend: [0.5, 0.7, 0.6, 0.9, 0.8, 0.75, 0.0], currentTimeOfDay: "Morning", currentTimeOfDaySymbol: "sunrise.fill", currentTimeOfDayRange: "Before 11am", nextRitualInfo: "Next: Drink Water (Midday)" ) } func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> WidgetEntry { await fetchLatestData(for: configuration, at: Date()) } func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { // Create entries for each time-of-day boundary for the next 24 hours. // This ensures the widget displays the correct rituals at each time period. var entries: [WidgetEntry] = [] let calendar = Calendar.current let now = Date() // Create an entry for right now let currentEntry = await fetchLatestData(for: configuration, at: now) entries.append(currentEntry) // Create entries at each upcoming time-of-day boundary let boundaryDates = upcomingTimeOfDayBoundaries(from: now, count: 5) for boundaryDate in boundaryDates { let entry = await fetchLatestData(for: configuration, at: boundaryDate) entries.append(entry) } // Request a new timeline after the last entry (roughly 24 hours) let lastEntryDate = entries.last?.date ?? now let nextRefresh = calendar.date(byAdding: .hour, value: 1, to: lastEntryDate) ?? lastEntryDate return Timeline(entries: entries, policy: .after(nextRefresh)) } /// Returns the next N time-of-day boundary dates. /// Boundaries are at 11:00, 14:00, 17:00, 21:00, and 00:00. private func upcomingTimeOfDayBoundaries(from startDate: Date, count: Int) -> [Date] { let calendar = Calendar.current let boundaryHours = [0, 11, 14, 17, 21] // midnight, morning end, midday end, afternoon end, evening end var boundaries: [Date] = [] var checkDate = startDate while boundaries.count < count { let currentHour = calendar.component(.hour, from: checkDate) let startOfDay = calendar.startOfDay(for: checkDate) // Find the next boundary hour after the current hour for hour in boundaryHours { if hour > currentHour { if let boundaryDate = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: startOfDay) { boundaries.append(boundaryDate) if boundaries.count >= count { break } } } } // Move to the next day for remaining boundaries if let nextDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) { checkDate = nextDay } else { break } } return boundaries } @MainActor private func fetchLatestData(for configuration: ConfigurationAppIntent, at targetDate: Date) -> WidgetEntry { let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self]) let configurationURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)? .appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite") let modelConfig = ModelConfiguration(schema: schema, url: configurationURL) do { let container = try ModelContainer(for: schema, configurations: [modelConfig]) let context = container.mainContext let descriptor = FetchDescriptor() let rituals = try context.fetch(descriptor) // Use targetDate for time-of-day calculation, but today for completion data let today = Calendar.current.startOfDay(for: targetDate) let dayID = RitualAnalytics.dayIdentifier(for: today) let timeOfDay = TimeOfDay.current(for: targetDate) // Filter rituals for the target time of day let activeRituals = rituals.filter { ritual in guard ritual.activeArc(on: today) != nil else { return false } return ritual.timeOfDay == .anytime || ritual.timeOfDay == timeOfDay } .sorted { lhs, rhs in if lhs.timeOfDay != rhs.timeOfDay { return lhs.timeOfDay < rhs.timeOfDay } return lhs.sortIndex < rhs.sortIndex } var visibleHabits: [HabitEntry] = [] for ritual in activeRituals { if let arc = ritual.activeArc(on: today) { // Sort habits within each ritual by their sortIndex let sortedHabits = (arc.habits ?? []).sorted { $0.sortIndex < $1.sortIndex } for habit in sortedHabits { visibleHabits.append(HabitEntry( id: habit.id, title: habit.title, symbolName: habit.symbolName, ritualTitle: ritual.title, isCompleted: habit.completedDayIDs.contains(dayID) )) } } } // Calculate overall progress across ALL rituals for today let overallRate = RitualAnalytics.overallCompletionRate(on: today, from: rituals) // Next habits (limit to 3) - still filtered by current time of day for the list let nextHabits = visibleHabits.prefix(3) // Streak calculation let streak = RitualAnalytics.calculateCurrentStreak(rituals: rituals) // Next ritual info var nextRitualString: String? = nil if let nextContext = RitualAnalytics.nextUpcomingRitualContext(from: rituals, currentDate: targetDate) { if nextContext.isTomorrow { let format = String(localized: "Next ritual: Tomorrow %@ (%@)") nextRitualString = String.localizedStringWithFormat( format, nextContext.ritual.timeOfDay.displayName, nextContext.ritual.timeOfDay.timeRange ) } else { let format = String(localized: "Next ritual: %@ (%@)") nextRitualString = String.localizedStringWithFormat( format, nextContext.ritual.timeOfDay.displayName, nextContext.ritual.timeOfDay.timeRange ) } } return WidgetEntry( date: targetDate, configuration: configuration, completionRate: overallRate, currentStreak: streak, nextHabits: Array(nextHabits), weeklyTrend: [], currentTimeOfDay: timeOfDay.displayName, currentTimeOfDaySymbol: timeOfDay.symbolName, currentTimeOfDayRange: timeOfDay.timeRange, nextRitualInfo: nextRitualString ) } catch { // Return a default entry instead of placeholder(in: .preview) return WidgetEntry( date: Date(), configuration: configuration, completionRate: 0.0, currentStreak: 0, nextHabits: [], weeklyTrend: [], currentTimeOfDay: "Today", currentTimeOfDaySymbol: "clock.fill", currentTimeOfDayRange: "", nextRitualInfo: nil ) } } }