Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-02 23:12:29 -06:00
parent eb6d77487e
commit c081d3c86a
5 changed files with 34 additions and 61 deletions

View File

@ -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
}

View File

@ -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) } }

View File

@ -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

View File

@ -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)

View File

@ -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))
}
}
}