Andromida/Andromida/App/Views/Rituals/RitualsView.swift

376 lines
13 KiB
Swift

import SwiftUI
import Bedrock
struct RitualsView: View {
@Bindable var store: RitualStore
@State private var selectedTab: RitualsTab = .current
@State private var showingPresetLibrary = false
@State private var showingCreateRitual = false
@State private var ritualToDelete: Ritual?
@State private var ritualToRestart: Ritual?
enum RitualsTab: String, CaseIterable {
case current
case past
var displayName: String {
switch self {
case .current: return String(localized: "Current")
case .past: return String(localized: "Past")
}
}
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: Design.Spacing.large) {
// Segmented picker
Picker(String(localized: "View"), selection: $selectedTab) {
ForEach(RitualsTab.allCases, id: \.self) { tab in
Text(tab.displayName).tag(tab)
}
}
.pickerStyle(.segmented)
.padding(.horizontal, Design.Spacing.small)
switch selectedTab {
case .current:
currentRitualsContent
case .past:
pastRitualsContent
}
}
.padding(Design.Spacing.large)
}
.background(LinearGradient(
colors: [AppSurface.primary, AppSurface.secondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.onAppear {
store.refresh()
}
.navigationTitle(String(localized: "Rituals"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Menu {
Button {
showingCreateRitual = true
} label: {
Label(String(localized: "Create New"), systemImage: "plus.circle")
}
Button {
showingPresetLibrary = true
} label: {
Label(String(localized: "Browse Presets"), systemImage: "sparkles.rectangle.stack")
}
} label: {
Image(systemName: "plus")
.foregroundStyle(AppAccent.primary)
}
}
}
.sheet(isPresented: $showingPresetLibrary) {
PresetLibrarySheet(store: store)
}
.sheet(isPresented: $showingCreateRitual) {
RitualEditSheet(store: store, ritual: nil)
}
.alert(String(localized: "Delete Ritual?"), isPresented: .init(
get: { ritualToDelete != nil },
set: { if !$0 { ritualToDelete = nil } }
)) {
Button(String(localized: "Cancel"), role: .cancel) {
ritualToDelete = nil
}
Button(String(localized: "Delete"), role: .destructive) {
if let ritual = ritualToDelete {
store.deleteRitual(ritual)
}
ritualToDelete = nil
}
} message: {
Text(String(localized: "This will permanently remove this ritual and all its completion history. This cannot be undone."))
}
.alert(String(localized: "Start New Arc?"), isPresented: .init(
get: { ritualToRestart != nil },
set: { if !$0 { ritualToRestart = nil } }
)) {
Button(String(localized: "Cancel"), role: .cancel) {
ritualToRestart = nil
}
Button(String(localized: "Start")) {
if let ritual = ritualToRestart {
store.startNewArc(for: ritual)
}
ritualToRestart = nil
}
} message: {
Text(String(localized: "This will start a new arc for this ritual with the same habits. You can modify habits after starting."))
}
}
// MARK: - Current Tab Content
@ViewBuilder
private var currentRitualsContent: some View {
let groupedRituals = store.currentRitualsGroupedByTime()
if groupedRituals.isEmpty {
currentEmptyState
} else {
ForEach(groupedRituals, id: \.timeOfDay) { group in
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
// Time of day header
HStack(spacing: Design.Spacing.small) {
Image(systemName: group.timeOfDay.symbolName)
.foregroundStyle(AppAccent.primary)
Text(group.timeOfDay.displayName)
.font(.subheadline)
.bold()
.foregroundStyle(AppTextColors.secondary)
}
.padding(.top, Design.Spacing.small)
ForEach(group.rituals) { ritual in
currentRitualRow(for: ritual)
}
}
}
}
}
// MARK: - Past Tab Content
@ViewBuilder
private var pastRitualsContent: some View {
if store.pastRituals.isEmpty {
pastEmptyState
} else {
VStack(spacing: Design.Spacing.medium) {
ForEach(store.pastRituals) { ritual in
pastRitualRow(for: ritual)
}
}
}
}
// MARK: - Empty States
private var currentEmptyState: some View {
VStack(spacing: Design.Spacing.large) {
Image(systemName: "sparkles")
.font(.system(size: Design.BaseFontSize.largeTitle * 2))
.foregroundStyle(AppAccent.primary)
Text(String(localized: "No Active Rituals"))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "Create a custom ritual or browse our preset library to get started."))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
HStack(spacing: Design.Spacing.medium) {
Button {
showingCreateRitual = true
} label: {
Label(String(localized: "Create"), systemImage: "plus")
}
.buttonStyle(.borderedProminent)
.tint(AppAccent.primary)
Button {
showingPresetLibrary = true
} label: {
Label(String(localized: "Presets"), systemImage: "sparkles.rectangle.stack")
}
.buttonStyle(.bordered)
}
if !store.pastRituals.isEmpty {
Text(String(localized: "Or restart a past ritual from the Past tab."))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
.padding(.top, Design.Spacing.small)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.xxxLarge)
}
private var pastEmptyState: some View {
VStack(spacing: Design.Spacing.large) {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: Design.BaseFontSize.largeTitle * 2))
.foregroundStyle(AppTextColors.tertiary)
Text(String(localized: "No Past Rituals"))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "Rituals that have ended will appear here. You can restart them anytime."))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.xxxLarge)
}
// MARK: - Ritual Rows
private func currentRitualRow(for ritual: Ritual) -> some View {
NavigationLink {
RitualDetailView(store: store, ritual: ritual)
} label: {
RitualCardView(
title: ritual.title,
theme: ritual.theme,
dayLabel: store.ritualDayLabel(for: ritual),
completionSummary: store.completionSummary(for: ritual),
iconName: ritual.iconName,
timeOfDay: ritual.timeOfDay,
hasActiveArc: true
)
}
.buttonStyle(.plain)
.contextMenu {
Button {
store.endArc(for: ritual)
} label: {
Label(String(localized: "End Arc"), systemImage: "stop.circle")
}
Divider()
Button(role: .destructive) {
ritualToDelete = ritual
} label: {
Label(String(localized: "Delete"), systemImage: "trash")
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
ritualToDelete = ritual
} label: {
Label(String(localized: "Delete"), systemImage: "trash")
}
Button {
store.endArc(for: ritual)
} label: {
Label(String(localized: "End"), systemImage: "stop.circle")
}
.tint(AppAccent.secondary)
}
}
private func pastRitualRow(for ritual: Ritual) -> some View {
NavigationLink {
RitualDetailView(store: store, ritual: ritual)
} label: {
pastRitualCardView(for: ritual)
}
.buttonStyle(.plain)
.contextMenu {
Button {
ritualToRestart = ritual
} label: {
Label(String(localized: "Start New Arc"), systemImage: "arrow.clockwise.circle")
}
Divider()
Button(role: .destructive) {
ritualToDelete = ritual
} label: {
Label(String(localized: "Delete"), systemImage: "trash")
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
ritualToDelete = ritual
} label: {
Label(String(localized: "Delete"), systemImage: "trash")
}
}
.swipeActions(edge: .leading, allowsFullSwipe: true) {
Button {
ritualToRestart = ritual
} label: {
Label(String(localized: "Restart"), systemImage: "arrow.clockwise.circle")
}
.tint(AppStatus.success)
}
}
private func pastRitualCardView(for ritual: Ritual) -> some View {
HStack(spacing: Design.Spacing.medium) {
// Icon
Image(systemName: ritual.iconName)
.font(.title2)
.foregroundStyle(AppTextColors.secondary)
.frame(width: 40, height: 40)
.background(AppSurface.secondary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.small))
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(ritual.title)
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(ritual.theme)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
if let lastArc = ritual.latestArc {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "calendar")
Text(formattedEndDate(lastArc.endDate))
}
.font(.caption2)
.foregroundStyle(AppTextColors.tertiary)
}
}
Spacer()
// Arc count badge
if ritual.completedArcCount > 0 {
Text("\(ritual.completedArcCount) arc\(ritual.completedArcCount == 1 ? "" : "s")")
.font(.caption2)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall)
.background(AppSurface.secondary)
.clipShape(.capsule)
}
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
.padding(Design.Spacing.medium)
.background(AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.opacity(Design.Opacity.heavy)
}
private func formattedEndDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return String(localized: "Ended \(formatter.string(from: date))")
}
}
#Preview {
NavigationStack {
RitualsView(store: RitualStore.preview)
}
}