122 lines
4.6 KiB
Swift
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)
|
|
}
|