Andromida/Andromida/App/Views/Today/TodayView.swift
Matt Bruce 713a65f64b fixed sync issue
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-02-16 09:17:27 -06:00

122 lines
4.6 KiB
Swift

import SwiftUI
import Bedrock
struct TodayView: View {
@Bindable var store: RitualStore
@Bindable var categoryStore: CategoryStore
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.scenePhase) private var scenePhase
/// Rituals to show now based on current time of day.
/// Depends on `store.currentTimeOfDay` which is observable.
private var todayRituals: [Ritual] {
// Access currentTimeOfDay to establish observation dependency
_ = store.currentTimeOfDay
return store.ritualsForToday()
}
/// Whether there are active rituals but none for the current time
private var hasRitualsButNotNow: Bool {
todayRituals.isEmpty && !store.currentRituals.isEmpty
}
/// Whether to show the renewal sheet
private var showRenewalSheet: Bool {
store.ritualNeedingRenewal != nil
}
/// Whether to use wide layout on iPad/landscape
private var useWideLayout: Bool {
horizontalSizeClass == .regular
}
/// Grid columns for ritual sections - 2 columns on regular width when multiple rituals
private var ritualColumns: [GridItem] {
AdaptiveColumns.columns(
compactCount: 1,
regularCount: todayRituals.count > 1 ? 2 : 1,
spacing: Design.Spacing.large,
horizontalSizeClass: horizontalSizeClass
)
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: Design.Spacing.large) {
if todayRituals.isEmpty {
if hasRitualsButNotNow {
// Has active rituals but none for current time of day
TodayNoRitualsForTimeView(store: store)
} else {
// No active rituals at all
TodayEmptyStateView(store: store, categoryStore: categoryStore)
}
} else {
// Use 2-column grid on iPad/landscape when multiple rituals
LazyVGrid(columns: ritualColumns, alignment: .leading, spacing: Design.Spacing.large) {
ForEach(todayRituals) { ritual in
TodayRitualSectionView(
focusTitle: ritual.title,
focusTheme: ritual.theme,
dayLabel: store.ritualDayLabel(for: ritual),
completionSummary: store.completionSummary(for: ritual),
progress: store.ritualProgress(for: ritual),
habitRows: habitRows(for: ritual),
iconName: ritual.iconName,
timeOfDay: ritual.timeOfDay
)
.frame(maxHeight: .infinity, alignment: .top)
}
}
}
}
.padding(Design.Spacing.large)
.adaptiveContentWidth()
}
.background(LinearGradient(
colors: [AppSurface.primary, AppSurface.secondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.refreshable { store.refresh() }
.navigationTitle(String(localized: "Today"))
.navigationBarTitleDisplayMode(horizontalSizeClass == .regular ? .inline : .large)
.sheet(isPresented: .init(
get: { showRenewalSheet },
set: { if !$0 { store.dismissRenewalPrompt() } }
)) {
if let ritual = store.ritualNeedingRenewal {
ArcRenewalSheet(store: store, categoryStore: categoryStore, ritual: ritual)
}
}
.onAppear {
store.updateCurrentTimeOfDay()
store.refreshIfNeeded()
}
.onChange(of: scenePhase) { _, newPhase in
// Check for time-of-day changes when app becomes active
if newPhase == .active {
if store.updateCurrentTimeOfDay() {
store.refresh()
}
}
}
}
private func habitRows(for ritual: Ritual) -> [HabitRowModel] {
store.habits(for: ritual).map { habit in
HabitRowModel(
id: habit.id,
title: habit.title,
symbolName: habit.symbolName,
isCompleted: store.isHabitCompletedToday(habit),
action: { store.toggleHabitCompletion(habit) }
)
}
}
}
#Preview {
TodayView(store: RitualStore.preview, categoryStore: CategoryStore.preview)
}