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

This commit is contained in:
Matt Bruce 2026-01-26 10:22:29 -06:00
parent 6aed4d319d
commit c29ae2bf74
10 changed files with 137 additions and 27 deletions

View File

@ -115,6 +115,14 @@ final class ReminderScheduler {
func clearBadge() { func clearBadge() {
UNUserNotificationCenter.current().setBadgeCount(0) { _ in } UNUserNotificationCenter.current().setBadgeCount(0) { _ in }
} }
/// Refreshes authorization status and reschedules if enabled.
func refreshStatus() async {
await refreshAuthorizationStatus()
if remindersEnabled {
await scheduleRemindersForActiveSlots()
}
}
// MARK: - Private // MARK: - Private

View File

@ -62,6 +62,12 @@ final class RitualStore: RitualStoreProviding {
return Double(completed) / Double(habits.count) return Double(completed) / Double(habits.count)
} }
/// Refreshes rituals and derived state for current date/time.
func refresh() {
reloadRituals()
checkForCompletedArcs()
}
func ritualProgress(for ritual: Ritual) -> Double { func ritualProgress(for ritual: Ritual) -> Double {
let habits = ritual.habits let habits = ritual.habits
guard !habits.isEmpty else { return 0 } guard !habits.isEmpty else { return 0 }

View File

@ -32,6 +32,10 @@ final class SettingsStore: CloudSyncable {
cloudSync.sync() cloudSync.sync()
} }
func refresh() {
cloudSync.sync()
}
private func update(_ transform: (inout AppSettingsData) -> Void) { private func update(_ transform: (inout AppSettingsData) -> Void) {
cloudSync.update { data in cloudSync.update { data in
transform(&data) transform(&data)

View File

@ -87,6 +87,9 @@ struct HistoryView: View {
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
)) ))
.onAppear {
store.refresh()
}
.sheet(item: $selectedDateItem) { item in .sheet(item: $selectedDateItem) { item in
HistoryDayDetailSheet( HistoryDayDetailSheet(
date: item.date, date: item.date,

View File

@ -29,6 +29,9 @@ struct InsightsView: View {
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
)) ))
.onAppear {
store.refresh()
}
} }
} }

View File

@ -30,26 +30,9 @@ struct RitualCardView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.small) { ViewThatFits(in: .horizontal) {
// Icon wideHeader
Image(systemName: iconName) compactHeader
.foregroundStyle(hasActiveArc ? AppAccent.primary : AppTextColors.tertiary)
.accessibilityHidden(true)
// Title
Text(title)
.font(.headline)
.foregroundStyle(hasActiveArc ? AppTextColors.primary : AppTextColors.tertiary)
Spacer(minLength: Design.Spacing.medium)
// Time of day badge - more prominent
timeOfDayBadge
// Day label
Text(dayLabel)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
} }
Text(theme) Text(theme)
@ -67,6 +50,74 @@ struct RitualCardView: View {
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
} }
// MARK: - Wide Layout (tablets/landscape)
private var wideHeader: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: iconName)
.foregroundStyle(hasActiveArc ? AppAccent.primary : AppTextColors.tertiary)
.accessibilityHidden(true)
Text(title)
.font(.headline)
.foregroundStyle(hasActiveArc ? AppTextColors.primary : AppTextColors.tertiary)
Spacer(minLength: Design.Spacing.medium)
timeOfDayBadge
Text(dayLabel)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.fixedSize()
}
}
// MARK: - Compact Layout (phones/portrait)
private var compactHeader: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack(spacing: Design.Spacing.small) {
Image(systemName: iconName)
.foregroundStyle(hasActiveArc ? AppAccent.primary : AppTextColors.tertiary)
.accessibilityHidden(true)
Text(title)
.font(.headline)
.foregroundStyle(hasActiveArc ? AppTextColors.primary : AppTextColors.tertiary)
}
HStack(spacing: Design.Spacing.small) {
compactTimeOfDayBadge
Text(dayLabel)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(AppSurface.secondary)
.clipShape(.capsule)
}
}
}
// MARK: - Time Badges
private var compactTimeOfDayBadge: some View {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: timeOfDay.symbolName)
.font(.caption2)
Text(timeOfDay.displayName)
.font(.caption2)
}
.foregroundStyle(timeOfDayColor)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(timeOfDayColor.opacity(0.15))
.clipShape(.capsule)
.accessibilityLabel(timeOfDay.displayNameWithRange)
}
private var timeOfDayBadge: some View { private var timeOfDayBadge: some View {
VStack(alignment: .trailing, spacing: 2) { VStack(alignment: .trailing, spacing: 2) {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {

View File

@ -47,6 +47,9 @@ struct RitualsView: View {
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
)) ))
.onAppear {
store.refresh()
}
.navigationTitle(String(localized: "Rituals")) .navigationTitle(String(localized: "Rituals"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {

View File

@ -6,34 +6,44 @@ struct RootView: View {
@Bindable var store: RitualStore @Bindable var store: RitualStore
@Bindable var settingsStore: SettingsStore @Bindable var settingsStore: SettingsStore
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false @AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
@Environment(\.scenePhase) private var scenePhase
@State private var selectedTab: RootTab = .today
enum RootTab: Hashable {
case today
case rituals
case insights
case history
case settings
}
var body: some View { var body: some View {
TabView { TabView(selection: $selectedTab) {
Tab(String(localized: "Today"), systemImage: "sun.max.fill") { Tab(String(localized: "Today"), systemImage: "sun.max.fill", value: RootTab.today) {
NavigationStack { NavigationStack {
TodayView(store: store) TodayView(store: store)
} }
} }
Tab(String(localized: "Rituals"), systemImage: "sparkles") { Tab(String(localized: "Rituals"), systemImage: "sparkles", value: RootTab.rituals) {
NavigationStack { NavigationStack {
RitualsView(store: store) RitualsView(store: store)
} }
} }
Tab(String(localized: "Insights"), systemImage: "chart.bar.fill") { Tab(String(localized: "Insights"), systemImage: "chart.bar.fill", value: RootTab.insights) {
NavigationStack { NavigationStack {
InsightsView(store: store) InsightsView(store: store)
} }
} }
Tab(String(localized: "History"), systemImage: "calendar") { Tab(String(localized: "History"), systemImage: "calendar", value: RootTab.history) {
NavigationStack { NavigationStack {
HistoryView(store: store) HistoryView(store: store)
} }
} }
Tab(String(localized: "Settings"), systemImage: "gearshape.fill") { Tab(String(localized: "Settings"), systemImage: "gearshape.fill", value: RootTab.settings) {
NavigationStack { NavigationStack {
SettingsView(store: settingsStore, ritualStore: store) SettingsView(store: settingsStore, ritualStore: store)
} }
@ -48,6 +58,21 @@ struct RootView: View {
delegate: self, delegate: self,
startDelay: Bedrock.Design.Animation.standard startDelay: Bedrock.Design.Animation.standard
) )
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
refreshCurrentTab()
}
}
}
private func refreshCurrentTab() {
store.refresh()
if selectedTab == .settings {
settingsStore.refresh()
Task {
await store.reminderScheduler.refreshStatus()
}
}
} }
} }

View File

@ -136,6 +136,13 @@ struct SettingsView: View {
} }
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
} }
.onAppear {
store.refresh()
ritualStore?.refresh()
Task {
await ritualStore?.reminderScheduler.refreshStatus()
}
}
.background(AppSurface.primary) .background(AppSurface.primary)
.navigationTitle(String(localized: "Settings")) .navigationTitle(String(localized: "Settings"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)

View File

@ -55,7 +55,7 @@ struct TodayView: View {
endPoint: .bottomTrailing endPoint: .bottomTrailing
)) ))
.onAppear { .onAppear {
store.checkForCompletedArcs() store.refresh()
} }
.sheet(isPresented: .init( .sheet(isPresented: .init(
get: { showRenewalSheet }, get: { showRenewalSheet },