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() {
UNUserNotificationCenter.current().setBadgeCount(0) { _ in }
}
/// Refreshes authorization status and reschedules if enabled.
func refreshStatus() async {
await refreshAuthorizationStatus()
if remindersEnabled {
await scheduleRemindersForActiveSlots()
}
}
// MARK: - Private

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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