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) } func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { let entry = await fetchLatestData(for: configuration) let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date() return Timeline(entries: [entry], policy: .after(nextUpdate)) } @MainActor private func fetchLatestData(for configuration: ConfigurationAppIntent) -> 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) let today = Date() let dayID = RitualAnalytics.dayIdentifier(for: today) let timeOfDay = TimeOfDay.current(for: today) // Match the app's logic for "Today" view let todayRituals = RitualAnalytics.ritualsActive(on: today, from: rituals) .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 todayRituals { if let arc = ritual.arcs?.first(where: { $0.isActive && $0.contains(date: 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 4) - still filtered by current time of day for the list let nextHabits = visibleHabits.prefix(4) // Streak calculation let streak = RitualAnalytics.calculateCurrentStreak(rituals: rituals) // Next ritual info var nextRitualString: String? = nil if let nextRitual = RitualAnalytics.nextUpcomingRitual(from: rituals, currentDate: today) { let isTomorrow = !Calendar.current.isDate(nextRitual.startDate, inSameDayAs: today) if isTomorrow { let format = String(localized: "Next ritual: Tomorrow %@ (%@)") nextRitualString = String.localizedStringWithFormat( format, nextRitual.timeOfDay.displayName, nextRitual.timeOfDay.timeRange ) } else { let format = String(localized: "Next ritual: %@ (%@)") nextRitualString = String.localizedStringWithFormat( format, nextRitual.timeOfDay.displayName, nextRitual.timeOfDay.timeRange ) } } return WidgetEntry( date: today, 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 ) } } }