diff --git a/Andromida/App/State/RitualStore.swift b/Andromida/App/State/RitualStore.swift index 4e01f8f..ea2c721 100644 --- a/Andromida/App/State/RitualStore.swift +++ b/Andromida/App/State/RitualStore.swift @@ -860,6 +860,9 @@ final class RitualStore: RitualStoreProviding { reloadRituals() // Notify widgets that data has changed WidgetCenter.shared.reloadAllTimelines() + // Trigger a UI refresh for observation-based views + analyticsNeedsRefresh = true + insightCardsNeedRefresh = true } catch { lastErrorMessage = error.localizedDescription } diff --git a/Andromida/App/Views/History/HistoryMonthView.swift b/Andromida/App/Views/History/HistoryMonthView.swift index d08bcdb..2b7251f 100644 --- a/Andromida/App/Views/History/HistoryMonthView.swift +++ b/Andromida/App/Views/History/HistoryMonthView.swift @@ -98,21 +98,18 @@ struct HistoryMonthView: View { private var dayGrid: some View { let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.xSmall), count: 7) let dates = daysInMonth - let progressValues = dates.map { date -> Double? in - guard let date else { return nil } - return date > today ? 0 : completionRate(date, selectedRitual) - } return LazyVGrid(columns: columns, spacing: Design.Spacing.xSmall) { ForEach(Array(dates.enumerated()), id: \.offset) { index, date in if let date = date { let isToday = calendar.isDate(date, inSameDayAs: today) let isFuture = date > today + let progress = isFuture ? 0 : completionRate(date, selectedRitual) // Show all days - future days show with 0 progress and are not tappable HistoryDayCell( date: date, - progress: progressValues[index] ?? 0, + progress: progress, isToday: isToday, isFuture: isFuture, onTap: { if !isFuture { onDayTapped(date) } } diff --git a/Andromida/App/Views/History/HistoryView.swift b/Andromida/App/Views/History/HistoryView.swift index 7ad61d0..e95cd24 100644 --- a/Andromida/App/Views/History/HistoryView.swift +++ b/Andromida/App/Views/History/HistoryView.swift @@ -22,7 +22,6 @@ struct HistoryView: View { @State private var selectedDateItem: IdentifiableDate? @State private var monthsToShow = 2 @State private var refreshToken = UUID() - @State private var cachedProgressByDate: [Date: Double] = [:] private let calendar = Calendar.current private let baseMonthsToShow = 2 @@ -93,8 +92,7 @@ struct HistoryView: View { month: month, selectedRitual: selectedRitual, completionRate: { date, ritual in - let day = calendar.startOfDay(for: date) - return cachedProgressByDate[day] ?? store.completionRate(for: day, ritual: ritual) + store.completionRate(for: date, ritual: ritual) }, onDayTapped: { date in selectedDateItem = IdentifiableDate(date: date) @@ -138,20 +136,16 @@ struct HistoryView: View { if let selectedRitual { self.selectedRitual = newRituals.first { $0.id == selectedRitual.id } } - refreshProgressCache() refreshToken = UUID() } .onChange(of: selectedRitual) { _, _ in - refreshProgressCache() refreshToken = UUID() } .onChange(of: monthsToShow) { _, _ in - refreshProgressCache() refreshToken = UUID() } .onAppear { store.refreshIfNeeded() - refreshProgressCache() } .sheet(item: $selectedDateItem) { item in HistoryDayDetailSheet( @@ -199,32 +193,6 @@ struct HistoryView: View { .accessibilityAddTraits(isSelected ? .isSelected : []) } - private func refreshProgressCache() { - Task { @MainActor in - await Task.yield() - let snapshotMonths = months - let selected = selectedRitual - var result: [Date: Double] = [:] - let today = calendar.startOfDay(for: Date()) - - for month in snapshotMonths { - guard let firstOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: month)), - let range = calendar.range(of: .day, in: .month, for: firstOfMonth) else { - continue - } - - for day in range { - guard let date = calendar.date(byAdding: .day, value: day - 1, to: firstOfMonth) else { continue } - let normalizedDate = calendar.startOfDay(for: date) - guard normalizedDate <= today else { continue } - result[normalizedDate] = store.completionRate(for: normalizedDate, ritual: selected) - } - } - let cache = result - cachedProgressByDate = cache - } - } - private func totalMonthsAvailable(from currentMonth: Date) -> Int { guard let earliestActivity = store.earliestActivityDate() else { return baseMonthsToShow } let earliestMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: earliestActivity)) ?? currentMonth diff --git a/AndromidaWidget/Providers/AndromidaWidgetProvider.swift b/AndromidaWidget/Providers/AndromidaWidgetProvider.swift index 3cf2a0e..f794383 100644 --- a/AndromidaWidget/Providers/AndromidaWidgetProvider.swift +++ b/AndromidaWidget/Providers/AndromidaWidgetProvider.swift @@ -139,8 +139,8 @@ struct AndromidaWidgetProvider: AppIntentTimelineProvider { // 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) + // 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) diff --git a/AndromidaWidget/Views/Components/LargeWidgetView.swift b/AndromidaWidget/Views/Components/LargeWidgetView.swift index fc4db36..7ecee73 100644 --- a/AndromidaWidget/Views/Components/LargeWidgetView.swift +++ b/AndromidaWidget/Views/Components/LargeWidgetView.swift @@ -31,6 +31,7 @@ struct LargeWidgetView: View { Divider() .background(AppTextColors.primary.opacity(0.2)) + .padding(.vertical, Design.Spacing.small) if entry.nextHabits.isEmpty { WidgetEmptyStateView( @@ -43,28 +44,32 @@ struct LargeWidgetView: View { isCompact: false ) } else { - Text(String(localized: "Habits")) - .styled(.captionEmphasis, emphasis: .custom(AppTextColors.secondary)) - - VStack(spacing: Design.Spacing.medium) { - ForEach(entry.nextHabits) { habit in - HStack(spacing: Design.Spacing.medium) { - Image(systemName: habit.symbolName) - .foregroundColor(AppAccent.primary) - .font(.system(size: 18)) - .frame(width: 24) - - VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { - Text(habit.title) - .styled(.subheading, emphasis: .custom(AppTextColors.primary)) - Text(habit.ritualTitle) - .styled(.caption, emphasis: .custom(AppTextColors.tertiary)) + VStack(alignment: .leading, spacing: Design.Spacing.small) { + Text(String(localized: "Habits")) + .styled(.captionEmphasis, emphasis: .custom(AppTextColors.secondary)) + + VStack(spacing: Design.Spacing.medium) { + ForEach(entry.nextHabits) { habit in + HStack(spacing: Design.Spacing.medium) { + Image(systemName: habit.symbolName) + .foregroundColor(AppAccent.primary) + .font(.system(size: 18)) + .frame(width: 24) + + VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { + Text(habit.title) + .styled(.subheading, emphasis: .custom(AppTextColors.primary)) + .lineLimit(1) + Text(habit.ritualTitle) + .styled(.caption, emphasis: .custom(AppTextColors.tertiary)) + .lineLimit(1) + } + Spacer() + + Image(systemName: habit.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundColor(habit.isCompleted ? .green : AppTextColors.primary.opacity(0.2)) + .font(.system(size: 20)) } - Spacer() - - Image(systemName: habit.isCompleted ? "checkmark.circle.fill" : "circle") - .foregroundColor(habit.isCompleted ? .green : AppTextColors.primary.opacity(0.2)) - .font(.system(size: 20)) } } }