Andromida/Andromida/App/Views/Rituals/Sheets/PresetLibrarySheet.swift

298 lines
11 KiB
Swift

import SwiftUI
import Bedrock
/// Sheet for browsing and adding preset rituals
struct PresetLibrarySheet: View {
@Bindable var store: RitualStore
@Environment(\.dismiss) private var dismiss
@State private var selectedCategory: PresetCategory = .health
@State private var selectedPreset: RitualPreset?
/// Track which presets have been added (by title match)
private var addedPresetTitles: Set<String> {
Set(store.rituals.map { $0.title })
}
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Category picker
categoryPicker
// Presets list
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: Design.Spacing.medium) {
ForEach(RitualPresetLibrary.presets(for: selectedCategory)) { preset in
presetCard(for: preset)
}
}
.padding(Design.Spacing.large)
}
}
.background(AppSurface.primary)
.navigationTitle(String(localized: "Preset Library"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "Done")) {
dismiss()
}
.foregroundStyle(AppAccent.primary)
}
}
.sheet(item: $selectedPreset) { preset in
PresetDetailSheet(store: store, preset: preset, isAlreadyAdded: addedPresetTitles.contains(preset.title))
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
// MARK: - Category Picker
private var categoryPicker: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.small) {
ForEach(PresetCategory.allCases, id: \.self) { category in
Button {
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
selectedCategory = category
}
} label: {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: category.symbolName)
Text(category.displayName)
}
.font(.subheadline)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(selectedCategory == category ? AppAccent.primary : AppSurface.card)
.foregroundStyle(selectedCategory == category ? .white : AppTextColors.primary)
.clipShape(.capsule)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
}
.background(AppSurface.secondary)
}
// MARK: - Preset Card
private func presetCard(for preset: RitualPreset) -> some View {
let isAdded = addedPresetTitles.contains(preset.title)
return Button {
selectedPreset = preset
} label: {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.medium) {
Image(systemName: preset.iconName)
.font(.title2)
.foregroundStyle(AppAccent.primary)
.frame(width: 40, height: 40)
.background(AppAccent.primary.opacity(0.1))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
HStack {
Text(preset.title)
.font(.headline)
.foregroundStyle(AppTextColors.primary)
if isAdded {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(AppStatus.success)
.font(.caption)
}
}
Text(preset.theme)
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: Design.Spacing.xSmall) {
Image(systemName: preset.timeOfDay.symbolName)
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
Text(String(localized: "\(preset.habits.count) habits"))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
}
// Habit preview
HStack(spacing: Design.Spacing.small) {
ForEach(preset.habits.prefix(4)) { habit in
Image(systemName: habit.symbolName)
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
if preset.habits.count > 4 {
Text("+\(preset.habits.count - 4)")
.font(.caption2)
.foregroundStyle(AppTextColors.tertiary)
}
}
}
.padding(Design.Spacing.large)
.background(AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
.buttonStyle(.plain)
}
}
/// Detail sheet for a single preset with full info and add button
struct PresetDetailSheet: View {
@Bindable var store: RitualStore
let preset: RitualPreset
let isAlreadyAdded: Bool
@Environment(\.dismiss) private var dismiss
@State private var hasBeenAdded = false
var body: some View {
NavigationStack {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: Design.Spacing.xLarge) {
// Header
headerSection
// Description
if !preset.notes.isEmpty {
descriptionSection
}
// Habits
habitsSection
// Add button
addButton
Spacer(minLength: Design.Spacing.xxxLarge)
}
.padding(Design.Spacing.large)
}
.background(AppSurface.primary)
.navigationTitle(preset.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "Done")) {
dismiss()
}
.foregroundStyle(AppAccent.primary)
}
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
private var headerSection: some View {
VStack(spacing: Design.Spacing.medium) {
SymbolIcon(preset.iconName, size: .hero, color: AppAccent.primary)
Text(preset.theme)
.typography(.body)
.foregroundStyle(AppTextColors.secondary)
HStack(spacing: Design.Spacing.large) {
Label(preset.timeOfDay.displayName, systemImage: preset.timeOfDay.symbolName)
Label(String(localized: "\(preset.durationDays) days"), systemImage: "calendar")
}
.typography(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
.frame(maxWidth: .infinity)
}
private var descriptionSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "About"))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(preset.notes)
.font(.body)
.foregroundStyle(AppTextColors.secondary)
}
.padding(Design.Spacing.large)
.frame(maxWidth: .infinity, alignment: .leading)
.background(AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
private var habitsSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Habits"))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
VStack(spacing: Design.Spacing.xSmall) {
ForEach(preset.habits) { habit in
HStack(spacing: Design.Spacing.medium) {
Image(systemName: habit.symbolName)
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text(habit.title)
.font(.subheadline)
.foregroundStyle(AppTextColors.primary)
Spacer()
}
.padding(.vertical, Design.Spacing.small)
if habit.id != preset.habits.last?.id {
Divider()
.background(AppBorder.subtle)
}
}
}
}
.padding(Design.Spacing.large)
.frame(maxWidth: .infinity, alignment: .leading)
.background(AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
private var addButton: some View {
Button {
store.createRitualFromPreset(preset)
hasBeenAdded = true
} label: {
HStack {
if isAlreadyAdded || hasBeenAdded {
Image(systemName: "checkmark.circle.fill")
Text(String(localized: "Added to My Rituals"))
} else {
Image(systemName: "plus.circle.fill")
Text(String(localized: "Add to My Rituals"))
}
}
.font(.headline)
.frame(maxWidth: .infinity)
.padding(Design.Spacing.medium)
.background(isAlreadyAdded || hasBeenAdded ? AppStatus.success : AppAccent.primary)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
}
.buttonStyle(.plain)
.disabled(isAlreadyAdded || hasBeenAdded)
}
}
#Preview {
PresetLibrarySheet(store: RitualStore.preview)
}