Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
eb6d77487e
commit
c081d3c86a
@ -860,6 +860,9 @@ final class RitualStore: RitualStoreProviding {
|
|||||||
reloadRituals()
|
reloadRituals()
|
||||||
// Notify widgets that data has changed
|
// Notify widgets that data has changed
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
// Trigger a UI refresh for observation-based views
|
||||||
|
analyticsNeedsRefresh = true
|
||||||
|
insightCardsNeedRefresh = true
|
||||||
} catch {
|
} catch {
|
||||||
lastErrorMessage = error.localizedDescription
|
lastErrorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,21 +98,18 @@ struct HistoryMonthView: View {
|
|||||||
private var dayGrid: some View {
|
private var dayGrid: some View {
|
||||||
let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.xSmall), count: 7)
|
let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.xSmall), count: 7)
|
||||||
let dates = daysInMonth
|
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) {
|
return LazyVGrid(columns: columns, spacing: Design.Spacing.xSmall) {
|
||||||
ForEach(Array(dates.enumerated()), id: \.offset) { index, date in
|
ForEach(Array(dates.enumerated()), id: \.offset) { index, date in
|
||||||
if let date = date {
|
if let date = date {
|
||||||
let isToday = calendar.isDate(date, inSameDayAs: today)
|
let isToday = calendar.isDate(date, inSameDayAs: today)
|
||||||
let isFuture = date > 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
|
// Show all days - future days show with 0 progress and are not tappable
|
||||||
HistoryDayCell(
|
HistoryDayCell(
|
||||||
date: date,
|
date: date,
|
||||||
progress: progressValues[index] ?? 0,
|
progress: progress,
|
||||||
isToday: isToday,
|
isToday: isToday,
|
||||||
isFuture: isFuture,
|
isFuture: isFuture,
|
||||||
onTap: { if !isFuture { onDayTapped(date) } }
|
onTap: { if !isFuture { onDayTapped(date) } }
|
||||||
|
|||||||
@ -22,7 +22,6 @@ struct HistoryView: View {
|
|||||||
@State private var selectedDateItem: IdentifiableDate?
|
@State private var selectedDateItem: IdentifiableDate?
|
||||||
@State private var monthsToShow = 2
|
@State private var monthsToShow = 2
|
||||||
@State private var refreshToken = UUID()
|
@State private var refreshToken = UUID()
|
||||||
@State private var cachedProgressByDate: [Date: Double] = [:]
|
|
||||||
|
|
||||||
private let calendar = Calendar.current
|
private let calendar = Calendar.current
|
||||||
private let baseMonthsToShow = 2
|
private let baseMonthsToShow = 2
|
||||||
@ -93,8 +92,7 @@ struct HistoryView: View {
|
|||||||
month: month,
|
month: month,
|
||||||
selectedRitual: selectedRitual,
|
selectedRitual: selectedRitual,
|
||||||
completionRate: { date, ritual in
|
completionRate: { date, ritual in
|
||||||
let day = calendar.startOfDay(for: date)
|
store.completionRate(for: date, ritual: ritual)
|
||||||
return cachedProgressByDate[day] ?? store.completionRate(for: day, ritual: ritual)
|
|
||||||
},
|
},
|
||||||
onDayTapped: { date in
|
onDayTapped: { date in
|
||||||
selectedDateItem = IdentifiableDate(date: date)
|
selectedDateItem = IdentifiableDate(date: date)
|
||||||
@ -138,20 +136,16 @@ struct HistoryView: View {
|
|||||||
if let selectedRitual {
|
if let selectedRitual {
|
||||||
self.selectedRitual = newRituals.first { $0.id == selectedRitual.id }
|
self.selectedRitual = newRituals.first { $0.id == selectedRitual.id }
|
||||||
}
|
}
|
||||||
refreshProgressCache()
|
|
||||||
refreshToken = UUID()
|
refreshToken = UUID()
|
||||||
}
|
}
|
||||||
.onChange(of: selectedRitual) { _, _ in
|
.onChange(of: selectedRitual) { _, _ in
|
||||||
refreshProgressCache()
|
|
||||||
refreshToken = UUID()
|
refreshToken = UUID()
|
||||||
}
|
}
|
||||||
.onChange(of: monthsToShow) { _, _ in
|
.onChange(of: monthsToShow) { _, _ in
|
||||||
refreshProgressCache()
|
|
||||||
refreshToken = UUID()
|
refreshToken = UUID()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
store.refreshIfNeeded()
|
store.refreshIfNeeded()
|
||||||
refreshProgressCache()
|
|
||||||
}
|
}
|
||||||
.sheet(item: $selectedDateItem) { item in
|
.sheet(item: $selectedDateItem) { item in
|
||||||
HistoryDayDetailSheet(
|
HistoryDayDetailSheet(
|
||||||
@ -199,32 +193,6 @@ struct HistoryView: View {
|
|||||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
.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 {
|
private func totalMonthsAvailable(from currentMonth: Date) -> Int {
|
||||||
guard let earliestActivity = store.earliestActivityDate() else { return baseMonthsToShow }
|
guard let earliestActivity = store.earliestActivityDate() else { return baseMonthsToShow }
|
||||||
let earliestMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: earliestActivity)) ?? currentMonth
|
let earliestMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: earliestActivity)) ?? currentMonth
|
||||||
|
|||||||
@ -139,8 +139,8 @@ struct AndromidaWidgetProvider: AppIntentTimelineProvider {
|
|||||||
// Calculate overall progress across ALL rituals for today
|
// Calculate overall progress across ALL rituals for today
|
||||||
let overallRate = RitualAnalytics.overallCompletionRate(on: today, from: rituals)
|
let overallRate = RitualAnalytics.overallCompletionRate(on: today, from: rituals)
|
||||||
|
|
||||||
// Next habits (limit to 4) - still filtered by current time of day for the list
|
// Next habits (limit to 3) - still filtered by current time of day for the list
|
||||||
let nextHabits = visibleHabits.prefix(4)
|
let nextHabits = visibleHabits.prefix(3)
|
||||||
|
|
||||||
// Streak calculation
|
// Streak calculation
|
||||||
let streak = RitualAnalytics.calculateCurrentStreak(rituals: rituals)
|
let streak = RitualAnalytics.calculateCurrentStreak(rituals: rituals)
|
||||||
|
|||||||
@ -31,6 +31,7 @@ struct LargeWidgetView: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.background(AppTextColors.primary.opacity(0.2))
|
.background(AppTextColors.primary.opacity(0.2))
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
|
||||||
if entry.nextHabits.isEmpty {
|
if entry.nextHabits.isEmpty {
|
||||||
WidgetEmptyStateView(
|
WidgetEmptyStateView(
|
||||||
@ -43,28 +44,32 @@ struct LargeWidgetView: View {
|
|||||||
isCompact: false
|
isCompact: false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(String(localized: "Habits"))
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
.styled(.captionEmphasis, emphasis: .custom(AppTextColors.secondary))
|
Text(String(localized: "Habits"))
|
||||||
|
.styled(.captionEmphasis, emphasis: .custom(AppTextColors.secondary))
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
|
||||||
ForEach(entry.nextHabits) { habit in
|
VStack(spacing: Design.Spacing.medium) {
|
||||||
HStack(spacing: Design.Spacing.medium) {
|
ForEach(entry.nextHabits) { habit in
|
||||||
Image(systemName: habit.symbolName)
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
.foregroundColor(AppAccent.primary)
|
Image(systemName: habit.symbolName)
|
||||||
.font(.system(size: 18))
|
.foregroundColor(AppAccent.primary)
|
||||||
.frame(width: 24)
|
.font(.system(size: 18))
|
||||||
|
.frame(width: 24)
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
|
||||||
Text(habit.title)
|
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||||
.styled(.subheading, emphasis: .custom(AppTextColors.primary))
|
Text(habit.title)
|
||||||
Text(habit.ritualTitle)
|
.styled(.subheading, emphasis: .custom(AppTextColors.primary))
|
||||||
.styled(.caption, emphasis: .custom(AppTextColors.tertiary))
|
.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user