From c29ae2bf74df6592cb7522737404784deaf9dba0 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 26 Jan 2026 10:22:29 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../App/Services/ReminderScheduler.swift | 8 ++ Andromida/App/State/RitualStore.swift | 6 ++ Andromida/App/State/SettingsStore.swift | 4 + Andromida/App/Views/History/HistoryView.swift | 3 + .../App/Views/Insights/InsightsView.swift | 3 + .../Rituals/Components/RitualCardView.swift | 91 +++++++++++++++---- Andromida/App/Views/Rituals/RitualsView.swift | 3 + Andromida/App/Views/RootView.swift | 37 ++++++-- .../App/Views/Settings/SettingsView.swift | 7 ++ Andromida/App/Views/Today/TodayView.swift | 2 +- 10 files changed, 137 insertions(+), 27 deletions(-) diff --git a/Andromida/App/Services/ReminderScheduler.swift b/Andromida/App/Services/ReminderScheduler.swift index 8aad148..98e2798 100644 --- a/Andromida/App/Services/ReminderScheduler.swift +++ b/Andromida/App/Services/ReminderScheduler.swift @@ -115,6 +115,14 @@ final class ReminderScheduler { func clearBadge() { UNUserNotificationCenter.current().setBadgeCount(0) { _ in } } + + /// Refreshes authorization status and reschedules if enabled. + func refreshStatus() async { + await refreshAuthorizationStatus() + if remindersEnabled { + await scheduleRemindersForActiveSlots() + } + } // MARK: - Private diff --git a/Andromida/App/State/RitualStore.swift b/Andromida/App/State/RitualStore.swift index 47c6b47..4fe3388 100644 --- a/Andromida/App/State/RitualStore.swift +++ b/Andromida/App/State/RitualStore.swift @@ -62,6 +62,12 @@ final class RitualStore: RitualStoreProviding { 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 { let habits = ritual.habits guard !habits.isEmpty else { return 0 } diff --git a/Andromida/App/State/SettingsStore.swift b/Andromida/App/State/SettingsStore.swift index 3da998a..fd00813 100644 --- a/Andromida/App/State/SettingsStore.swift +++ b/Andromida/App/State/SettingsStore.swift @@ -32,6 +32,10 @@ final class SettingsStore: CloudSyncable { cloudSync.sync() } + func refresh() { + cloudSync.sync() + } + private func update(_ transform: (inout AppSettingsData) -> Void) { cloudSync.update { data in transform(&data) diff --git a/Andromida/App/Views/History/HistoryView.swift b/Andromida/App/Views/History/HistoryView.swift index 5832163..1cf3752 100644 --- a/Andromida/App/Views/History/HistoryView.swift +++ b/Andromida/App/Views/History/HistoryView.swift @@ -87,6 +87,9 @@ struct HistoryView: View { startPoint: .topLeading, endPoint: .bottomTrailing )) + .onAppear { + store.refresh() + } .sheet(item: $selectedDateItem) { item in HistoryDayDetailSheet( date: item.date, diff --git a/Andromida/App/Views/Insights/InsightsView.swift b/Andromida/App/Views/Insights/InsightsView.swift index fb498ea..8415b4b 100644 --- a/Andromida/App/Views/Insights/InsightsView.swift +++ b/Andromida/App/Views/Insights/InsightsView.swift @@ -29,6 +29,9 @@ struct InsightsView: View { startPoint: .topLeading, endPoint: .bottomTrailing )) + .onAppear { + store.refresh() + } } } diff --git a/Andromida/App/Views/Rituals/Components/RitualCardView.swift b/Andromida/App/Views/Rituals/Components/RitualCardView.swift index 4177e9c..9fef6e6 100644 --- a/Andromida/App/Views/Rituals/Components/RitualCardView.swift +++ b/Andromida/App/Views/Rituals/Components/RitualCardView.swift @@ -30,26 +30,9 @@ struct RitualCardView: View { var body: some View { VStack(alignment: .leading, spacing: Design.Spacing.medium) { - HStack(spacing: Design.Spacing.small) { - // Icon - Image(systemName: iconName) - .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) + ViewThatFits(in: .horizontal) { + wideHeader + compactHeader } Text(theme) @@ -67,6 +50,74 @@ struct RitualCardView: View { .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 { VStack(alignment: .trailing, spacing: 2) { HStack(spacing: Design.Spacing.xSmall) { diff --git a/Andromida/App/Views/Rituals/RitualsView.swift b/Andromida/App/Views/Rituals/RitualsView.swift index 564687c..86b41e6 100644 --- a/Andromida/App/Views/Rituals/RitualsView.swift +++ b/Andromida/App/Views/Rituals/RitualsView.swift @@ -47,6 +47,9 @@ struct RitualsView: View { startPoint: .topLeading, endPoint: .bottomTrailing )) + .onAppear { + store.refresh() + } .navigationTitle(String(localized: "Rituals")) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Andromida/App/Views/RootView.swift b/Andromida/App/Views/RootView.swift index a7d26c3..2e0fc63 100644 --- a/Andromida/App/Views/RootView.swift +++ b/Andromida/App/Views/RootView.swift @@ -6,34 +6,44 @@ struct RootView: View { @Bindable var store: RitualStore @Bindable var settingsStore: SettingsStore @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 { - TabView { - Tab(String(localized: "Today"), systemImage: "sun.max.fill") { + TabView(selection: $selectedTab) { + Tab(String(localized: "Today"), systemImage: "sun.max.fill", value: RootTab.today) { NavigationStack { TodayView(store: store) } } - Tab(String(localized: "Rituals"), systemImage: "sparkles") { + Tab(String(localized: "Rituals"), systemImage: "sparkles", value: RootTab.rituals) { NavigationStack { RitualsView(store: store) } } - Tab(String(localized: "Insights"), systemImage: "chart.bar.fill") { + Tab(String(localized: "Insights"), systemImage: "chart.bar.fill", value: RootTab.insights) { NavigationStack { InsightsView(store: store) } } - Tab(String(localized: "History"), systemImage: "calendar") { + Tab(String(localized: "History"), systemImage: "calendar", value: RootTab.history) { NavigationStack { HistoryView(store: store) } } - Tab(String(localized: "Settings"), systemImage: "gearshape.fill") { + Tab(String(localized: "Settings"), systemImage: "gearshape.fill", value: RootTab.settings) { NavigationStack { SettingsView(store: settingsStore, ritualStore: store) } @@ -48,6 +58,21 @@ struct RootView: View { delegate: self, 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() + } + } } } diff --git a/Andromida/App/Views/Settings/SettingsView.swift b/Andromida/App/Views/Settings/SettingsView.swift index b8ebcf9..6610e71 100644 --- a/Andromida/App/Views/Settings/SettingsView.swift +++ b/Andromida/App/Views/Settings/SettingsView.swift @@ -136,6 +136,13 @@ struct SettingsView: View { } .padding(.horizontal, Design.Spacing.large) } + .onAppear { + store.refresh() + ritualStore?.refresh() + Task { + await ritualStore?.reminderScheduler.refreshStatus() + } + } .background(AppSurface.primary) .navigationTitle(String(localized: "Settings")) .navigationBarTitleDisplayMode(.inline) diff --git a/Andromida/App/Views/Today/TodayView.swift b/Andromida/App/Views/Today/TodayView.swift index 0702b7b..1c94c57 100644 --- a/Andromida/App/Views/Today/TodayView.swift +++ b/Andromida/App/Views/Today/TodayView.swift @@ -55,7 +55,7 @@ struct TodayView: View { endPoint: .bottomTrailing )) .onAppear { - store.checkForCompletedArcs() + store.refresh() } .sheet(isPresented: .init( get: { showRenewalSheet },