Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
0223692513
commit
99d1db84e9
@ -8,6 +8,7 @@ struct AndromidaApp: App {
|
|||||||
private let modelContainer: ModelContainer
|
private let modelContainer: ModelContainer
|
||||||
@State private var store: RitualStore
|
@State private var store: RitualStore
|
||||||
@State private var settingsStore: SettingsStore
|
@State private var settingsStore: SettingsStore
|
||||||
|
@State private var categoryStore: CategoryStore
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
||||||
@ -22,6 +23,7 @@ struct AndromidaApp: App {
|
|||||||
modelContainer = container
|
modelContainer = container
|
||||||
let settings = SettingsStore()
|
let settings = SettingsStore()
|
||||||
_settingsStore = State(initialValue: settings)
|
_settingsStore = State(initialValue: settings)
|
||||||
|
_categoryStore = State(initialValue: CategoryStore())
|
||||||
_store = State(initialValue: RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings))
|
_store = State(initialValue: RitualStore(modelContext: container.mainContext, seedService: RitualSeedService(), settingsStore: settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ struct AndromidaApp: App {
|
|||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
AppLaunchView(config: .rituals) {
|
AppLaunchView(config: .rituals) {
|
||||||
RootView(store: store, settingsStore: settingsStore)
|
RootView(store: store, settingsStore: settingsStore, categoryStore: categoryStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
64
Andromida/App/Models/Category.swift
Normal file
64
Andromida/App/Models/Category.swift
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Represents a category for organizing rituals.
|
||||||
|
/// Categories can be global presets (non-deletable) or user-created.
|
||||||
|
struct Category: Identifiable, Codable, Equatable, Hashable {
|
||||||
|
let id: UUID
|
||||||
|
var name: String
|
||||||
|
var colorName: String
|
||||||
|
let isPreset: Bool
|
||||||
|
|
||||||
|
/// Available colors for categories
|
||||||
|
static let availableColors: [(name: String, color: Color)] = [
|
||||||
|
("red", .red),
|
||||||
|
("orange", .orange),
|
||||||
|
("yellow", .yellow),
|
||||||
|
("green", .green),
|
||||||
|
("mint", .mint),
|
||||||
|
("teal", .teal),
|
||||||
|
("cyan", .cyan),
|
||||||
|
("blue", .blue),
|
||||||
|
("indigo", .indigo),
|
||||||
|
("purple", .purple),
|
||||||
|
("pink", .pink),
|
||||||
|
("brown", .brown),
|
||||||
|
("gray", .gray)
|
||||||
|
]
|
||||||
|
|
||||||
|
/// The SwiftUI Color for this category
|
||||||
|
var color: Color {
|
||||||
|
Self.color(for: colorName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert color name to SwiftUI Color
|
||||||
|
static func color(for name: String) -> Color {
|
||||||
|
availableColors.first { $0.name == name }?.color ?? .gray
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert SwiftUI Color to storable name
|
||||||
|
static func colorName(for color: Color) -> String {
|
||||||
|
availableColors.first { $0.color == color }?.name ?? "gray"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Presets
|
||||||
|
|
||||||
|
/// Fixed UUIDs for presets to ensure consistency across app launches
|
||||||
|
private static let healthID = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
|
||||||
|
private static let productivityID = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
|
||||||
|
private static let mindfulnessID = UUID(uuidString: "00000000-0000-0000-0000-000000000003")!
|
||||||
|
private static let selfCareID = UUID(uuidString: "00000000-0000-0000-0000-000000000004")!
|
||||||
|
|
||||||
|
/// Default preset categories
|
||||||
|
static let defaultPresets: [Category] = [
|
||||||
|
Category(id: healthID, name: "Health", colorName: "red", isPreset: true),
|
||||||
|
Category(id: productivityID, name: "Productivity", colorName: "blue", isPreset: true),
|
||||||
|
Category(id: mindfulnessID, name: "Mindfulness", colorName: "purple", isPreset: true),
|
||||||
|
Category(id: selfCareID, name: "Self-Care", colorName: "pink", isPreset: true)
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Create a new user category
|
||||||
|
static func create(name: String, colorName: String) -> Category {
|
||||||
|
Category(id: UUID(), name: name, colorName: colorName, isPreset: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
/// A template for a habit within a preset ritual
|
/// A template for a habit within a preset ritual
|
||||||
struct HabitPreset: Identifiable {
|
struct HabitPreset: Identifiable {
|
||||||
@ -20,7 +21,7 @@ struct RitualPreset: Identifiable {
|
|||||||
let habits: [HabitPreset]
|
let habits: [HabitPreset]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Categories for organizing presets
|
/// Categories for organizing presets in the preset library
|
||||||
enum PresetCategory: String, CaseIterable {
|
enum PresetCategory: String, CaseIterable {
|
||||||
case health = "Health"
|
case health = "Health"
|
||||||
case productivity = "Productivity"
|
case productivity = "Productivity"
|
||||||
|
|||||||
150
Andromida/App/State/CategoryStore.swift
Normal file
150
Andromida/App/State/CategoryStore.swift
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Manages categories for rituals, including global presets and user-created categories.
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class CategoryStore {
|
||||||
|
|
||||||
|
private let userDefaultsKey = "userCategories"
|
||||||
|
private let presetColorsKey = "presetCategoryColors"
|
||||||
|
|
||||||
|
/// All categories (presets + user-created), sorted alphabetically with presets first
|
||||||
|
private(set) var categories: [Category] = []
|
||||||
|
|
||||||
|
init() {
|
||||||
|
loadCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
|
/// Get a category by name
|
||||||
|
func category(named name: String) -> Category? {
|
||||||
|
categories.first { $0.name == name }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get color for a category name, with fallback
|
||||||
|
func color(for categoryName: String) -> Color {
|
||||||
|
category(named: categoryName)?.color ?? .gray
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new user category
|
||||||
|
func addCategory(name: String, colorName: String) {
|
||||||
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmedName.isEmpty else { return }
|
||||||
|
|
||||||
|
// Don't add if name already exists
|
||||||
|
guard category(named: trimmedName) == nil else { return }
|
||||||
|
|
||||||
|
let newCategory = Category.create(name: trimmedName, colorName: colorName)
|
||||||
|
categories.append(newCategory)
|
||||||
|
sortCategories()
|
||||||
|
saveUserCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a category's properties
|
||||||
|
func updateCategory(_ category: Category, name: String? = nil, colorName: String? = nil) {
|
||||||
|
guard let index = categories.firstIndex(where: { $0.id == category.id }) else { return }
|
||||||
|
|
||||||
|
if category.isPreset {
|
||||||
|
// Presets can only change color
|
||||||
|
if let colorName {
|
||||||
|
categories[index].colorName = colorName
|
||||||
|
savePresetColors()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User categories can change name and color
|
||||||
|
if let name {
|
||||||
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
// Ensure no duplicate names
|
||||||
|
if !trimmedName.isEmpty && self.category(named: trimmedName) == nil {
|
||||||
|
categories[index].name = trimmedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let colorName {
|
||||||
|
categories[index].colorName = colorName
|
||||||
|
}
|
||||||
|
sortCategories()
|
||||||
|
saveUserCategories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a user category (presets cannot be deleted)
|
||||||
|
func deleteCategory(_ category: Category) {
|
||||||
|
guard !category.isPreset else { return }
|
||||||
|
categories.removeAll { $0.id == category.id }
|
||||||
|
saveUserCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a category name is available
|
||||||
|
func isNameAvailable(_ name: String, excluding: Category? = nil) -> Bool {
|
||||||
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return categories.first {
|
||||||
|
$0.name.lowercased() == trimmedName.lowercased() && $0.id != excluding?.id
|
||||||
|
} == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Persistence
|
||||||
|
|
||||||
|
private func loadCategories() {
|
||||||
|
// Start with default presets
|
||||||
|
var loadedCategories = Category.defaultPresets
|
||||||
|
|
||||||
|
// Apply saved preset colors
|
||||||
|
if let presetColors = UserDefaults.standard.dictionary(forKey: presetColorsKey) as? [String: String] {
|
||||||
|
for i in loadedCategories.indices where loadedCategories[i].isPreset {
|
||||||
|
if let savedColor = presetColors[loadedCategories[i].name] {
|
||||||
|
loadedCategories[i].colorName = savedColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user categories
|
||||||
|
if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
|
||||||
|
let userCategories = try? JSONDecoder().decode([Category].self, from: data) {
|
||||||
|
loadedCategories.append(contentsOf: userCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = loadedCategories
|
||||||
|
sortCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveUserCategories() {
|
||||||
|
let userCategories = categories.filter { !$0.isPreset }
|
||||||
|
if let data = try? JSONEncoder().encode(userCategories) {
|
||||||
|
UserDefaults.standard.set(data, forKey: userDefaultsKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func savePresetColors() {
|
||||||
|
let presetColors = categories
|
||||||
|
.filter { $0.isPreset }
|
||||||
|
.reduce(into: [String: String]()) { dict, cat in
|
||||||
|
dict[cat.name] = cat.colorName
|
||||||
|
}
|
||||||
|
UserDefaults.standard.set(presetColors, forKey: presetColorsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sortCategories() {
|
||||||
|
// Presets first (in their defined order), then user categories alphabetically
|
||||||
|
let presets = categories.filter { $0.isPreset }.sorted { cat1, cat2 in
|
||||||
|
let idx1 = Category.defaultPresets.firstIndex(where: { $0.id == cat1.id }) ?? 0
|
||||||
|
let idx2 = Category.defaultPresets.firstIndex(where: { $0.id == cat2.id }) ?? 0
|
||||||
|
return idx1 < idx2
|
||||||
|
}
|
||||||
|
let userCats = categories.filter { !$0.isPreset }.sorted { $0.name < $1.name }
|
||||||
|
categories = presets + userCats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview Support
|
||||||
|
|
||||||
|
extension CategoryStore {
|
||||||
|
static var preview: CategoryStore {
|
||||||
|
let store = CategoryStore()
|
||||||
|
store.addCategory(name: "Fitness", colorName: "green")
|
||||||
|
store.addCategory(name: "Learning", colorName: "orange")
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import Bedrock
|
|||||||
|
|
||||||
struct RitualDetailView: View {
|
struct RitualDetailView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
private let ritual: Ritual
|
private let ritual: Ritual
|
||||||
@ -12,8 +13,9 @@ struct RitualDetailView: View {
|
|||||||
@State private var showingEndArcConfirmation = false
|
@State private var showingEndArcConfirmation = false
|
||||||
@State private var showingStartArcConfirmation = false
|
@State private var showingStartArcConfirmation = false
|
||||||
|
|
||||||
init(store: RitualStore, ritual: Ritual) {
|
init(store: RitualStore, categoryStore: CategoryStore, ritual: Ritual) {
|
||||||
self.store = store
|
self.store = store
|
||||||
|
self.categoryStore = categoryStore
|
||||||
self.ritual = ritual
|
self.ritual = ritual
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +123,7 @@ struct RitualDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingEditSheet) {
|
.sheet(isPresented: $showingEditSheet) {
|
||||||
RitualEditSheet(store: store, ritual: ritual)
|
RitualEditSheet(store: store, categoryStore: categoryStore, ritual: ritual)
|
||||||
}
|
}
|
||||||
.alert(String(localized: "Delete Ritual?"), isPresented: $showingDeleteConfirmation) {
|
.alert(String(localized: "Delete Ritual?"), isPresented: $showingDeleteConfirmation) {
|
||||||
Button(String(localized: "Cancel"), role: .cancel) {}
|
Button(String(localized: "Cancel"), role: .cancel) {}
|
||||||
@ -297,13 +299,14 @@ struct RitualDetailView: View {
|
|||||||
|
|
||||||
// Category badge (if set)
|
// Category badge (if set)
|
||||||
if !ritual.category.isEmpty {
|
if !ritual.category.isEmpty {
|
||||||
|
let badgeColor = categoryStore.color(for: ritual.category)
|
||||||
Text(ritual.category)
|
Text(ritual.category)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(.horizontal, Design.Spacing.small)
|
.padding(.horizontal, Design.Spacing.small)
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
.background(AppSurface.card)
|
.background(badgeColor.opacity(0.15))
|
||||||
.clipShape(.capsule)
|
.clipShape(.capsule)
|
||||||
.foregroundStyle(AppTextColors.secondary)
|
.foregroundStyle(badgeColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -456,6 +459,6 @@ struct RitualDetailView: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RitualDetailView(store: RitualStore.preview, ritual: RitualStore.preview.rituals.first!)
|
RitualDetailView(store: RitualStore.preview, categoryStore: CategoryStore.preview, ritual: RitualStore.preview.rituals.first!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Bedrock
|
|||||||
|
|
||||||
struct RitualsView: View {
|
struct RitualsView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
@State private var selectedTab: RitualsTab = .current
|
@State private var selectedTab: RitualsTab = .current
|
||||||
@State private var showingPresetLibrary = false
|
@State private var showingPresetLibrary = false
|
||||||
@State private var showingCreateRitual = false
|
@State private var showingCreateRitual = false
|
||||||
@ -76,7 +77,7 @@ struct RitualsView: View {
|
|||||||
PresetLibrarySheet(store: store)
|
PresetLibrarySheet(store: store)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingCreateRitual) {
|
.sheet(isPresented: $showingCreateRitual) {
|
||||||
RitualEditSheet(store: store, ritual: nil)
|
RitualEditSheet(store: store, categoryStore: categoryStore, ritual: nil)
|
||||||
}
|
}
|
||||||
.alert(String(localized: "Delete Ritual?"), isPresented: .init(
|
.alert(String(localized: "Delete Ritual?"), isPresented: .init(
|
||||||
get: { ritualToDelete != nil },
|
get: { ritualToDelete != nil },
|
||||||
@ -225,7 +226,7 @@ struct RitualsView: View {
|
|||||||
|
|
||||||
private func currentRitualRow(for ritual: Ritual) -> some View {
|
private func currentRitualRow(for ritual: Ritual) -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
RitualDetailView(store: store, ritual: ritual)
|
RitualDetailView(store: store, categoryStore: categoryStore, ritual: ritual)
|
||||||
} label: {
|
} label: {
|
||||||
RitualCardView(
|
RitualCardView(
|
||||||
title: ritual.title,
|
title: ritual.title,
|
||||||
@ -271,7 +272,7 @@ struct RitualsView: View {
|
|||||||
|
|
||||||
private func pastRitualRow(for ritual: Ritual) -> some View {
|
private func pastRitualRow(for ritual: Ritual) -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
RitualDetailView(store: store, ritual: ritual)
|
RitualDetailView(store: store, categoryStore: categoryStore, ritual: ritual)
|
||||||
} label: {
|
} label: {
|
||||||
pastRitualCardView(for: ritual)
|
pastRitualCardView(for: ritual)
|
||||||
}
|
}
|
||||||
@ -370,6 +371,6 @@ struct RitualsView: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RitualsView(store: RitualStore.preview)
|
RitualsView(store: RitualStore.preview, categoryStore: CategoryStore.preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,14 +4,16 @@ import Bedrock
|
|||||||
/// Sheet presented when a ritual's arc completes, prompting the user to renew or end.
|
/// Sheet presented when a ritual's arc completes, prompting the user to renew or end.
|
||||||
struct ArcRenewalSheet: View {
|
struct ArcRenewalSheet: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
let ritual: Ritual
|
let ritual: Ritual
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var durationDays: Double
|
@State private var durationDays: Double
|
||||||
@State private var showingEditSheet = false
|
@State private var showingEditSheet = false
|
||||||
|
|
||||||
init(store: RitualStore, ritual: Ritual) {
|
init(store: RitualStore, categoryStore: CategoryStore, ritual: Ritual) {
|
||||||
self.store = store
|
self.store = store
|
||||||
|
self.categoryStore = categoryStore
|
||||||
self.ritual = ritual
|
self.ritual = ritual
|
||||||
_durationDays = State(initialValue: Double(ritual.defaultDurationDays))
|
_durationDays = State(initialValue: Double(ritual.defaultDurationDays))
|
||||||
}
|
}
|
||||||
@ -53,7 +55,7 @@ struct ArcRenewalSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingEditSheet) {
|
.sheet(isPresented: $showingEditSheet) {
|
||||||
RitualEditSheet(store: store, ritual: ritual)
|
RitualEditSheet(store: store, categoryStore: categoryStore, ritual: ritual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import Bedrock
|
|||||||
/// Sheet for creating or editing a ritual
|
/// Sheet for creating or editing a ritual
|
||||||
struct RitualEditSheet: View {
|
struct RitualEditSheet: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
let ritual: Ritual?
|
let ritual: Ritual?
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@ -15,9 +16,7 @@ struct RitualEditSheet: View {
|
|||||||
@State private var durationDays: Double = 28
|
@State private var durationDays: Double = 28
|
||||||
@State private var timeOfDay: TimeOfDay = .anytime
|
@State private var timeOfDay: TimeOfDay = .anytime
|
||||||
@State private var iconName: String = "sparkles"
|
@State private var iconName: String = "sparkles"
|
||||||
@State private var category: String = ""
|
@State private var selectedCategory: String = ""
|
||||||
@State private var customCategory: String = ""
|
|
||||||
@State private var isUsingCustomCategory: Bool = false
|
|
||||||
@State private var habits: [EditableHabit] = []
|
@State private var habits: [EditableHabit] = []
|
||||||
|
|
||||||
@State private var showingIconPicker = false
|
@State private var showingIconPicker = false
|
||||||
@ -32,8 +31,9 @@ struct RitualEditSheet: View {
|
|||||||
!title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
!title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
init(store: RitualStore, ritual: Ritual?) {
|
init(store: RitualStore, categoryStore: CategoryStore, ritual: Ritual?) {
|
||||||
self.store = store
|
self.store = store
|
||||||
|
self.categoryStore = categoryStore
|
||||||
self.ritual = ritual
|
self.ritual = ritual
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,22 +122,35 @@ struct RitualEditSheet: View {
|
|||||||
}
|
}
|
||||||
.listRowBackground(AppSurface.card)
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
// Category selection with custom option
|
// Category selection - simple picker from CategoryStore
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||||
Picker(String(localized: "Category"), selection: $category) {
|
Text(String(localized: "Category"))
|
||||||
Text(String(localized: "None")).tag("")
|
.font(.subheadline)
|
||||||
ForEach(PresetCategory.allCases, id: \.self) { cat in
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
Text(cat.displayName).tag(cat.rawValue)
|
|
||||||
}
|
|
||||||
Text(String(localized: "Custom...")).tag("__custom__")
|
|
||||||
}
|
|
||||||
.onChange(of: category) { _, newValue in
|
|
||||||
isUsingCustomCategory = (newValue == "__custom__")
|
|
||||||
}
|
|
||||||
|
|
||||||
if isUsingCustomCategory {
|
// Horizontal scrollable category chips
|
||||||
TextField(String(localized: "Enter custom category"), text: $customCategory)
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
.textFieldStyle(.roundedBorder)
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
// None option
|
||||||
|
categoryChip(
|
||||||
|
label: String(localized: "None"),
|
||||||
|
color: nil,
|
||||||
|
isSelected: selectedCategory.isEmpty
|
||||||
|
) {
|
||||||
|
selectedCategory = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// All categories from store
|
||||||
|
ForEach(categoryStore.categories) { cat in
|
||||||
|
categoryChip(
|
||||||
|
label: cat.name,
|
||||||
|
color: cat.color,
|
||||||
|
isSelected: selectedCategory == cat.name
|
||||||
|
) {
|
||||||
|
selectedCategory = cat.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(AppSurface.card)
|
.listRowBackground(AppSurface.card)
|
||||||
@ -357,19 +370,8 @@ struct RitualEditSheet: View {
|
|||||||
iconName = ritual.iconName
|
iconName = ritual.iconName
|
||||||
habits = ritual.habits.map { EditableHabit(from: $0) }
|
habits = ritual.habits.map { EditableHabit(from: $0) }
|
||||||
|
|
||||||
// Check if category is a preset or custom
|
// Load category
|
||||||
let presetCategories = PresetCategory.allCases.map { $0.rawValue }
|
selectedCategory = ritual.category
|
||||||
if ritual.category.isEmpty {
|
|
||||||
category = ""
|
|
||||||
isUsingCustomCategory = false
|
|
||||||
} else if presetCategories.contains(ritual.category) {
|
|
||||||
category = ritual.category
|
|
||||||
isUsingCustomCategory = false
|
|
||||||
} else {
|
|
||||||
category = "__custom__"
|
|
||||||
customCategory = ritual.category
|
|
||||||
isUsingCustomCategory = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addNewHabit() {
|
private func addNewHabit() {
|
||||||
@ -386,16 +388,32 @@ struct RitualEditSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var effectiveCategory: String {
|
private func categoryChip(
|
||||||
if isUsingCustomCategory {
|
label: String,
|
||||||
return customCategory.trimmingCharacters(in: .whitespacesAndNewlines)
|
color: Color?,
|
||||||
} else if category == "__custom__" {
|
isSelected: Bool,
|
||||||
return ""
|
action: @escaping () -> Void
|
||||||
} else {
|
) -> some View {
|
||||||
return category
|
Button(action: action) {
|
||||||
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
|
if let color {
|
||||||
|
Circle()
|
||||||
|
.fill(color)
|
||||||
|
.frame(width: 10, height: 10)
|
||||||
|
}
|
||||||
|
Text(label)
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, Design.Spacing.medium)
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
.background(isSelected ? (color ?? AppAccent.primary) : AppSurface.secondary)
|
||||||
|
.foregroundStyle(isSelected ? .white : AppTextColors.primary)
|
||||||
|
.clipShape(.capsule)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func saveRitual() {
|
private func saveRitual() {
|
||||||
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedTheme = theme.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedTheme = theme.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@ -411,7 +429,7 @@ struct RitualEditSheet: View {
|
|||||||
durationDays: Int(durationDays),
|
durationDays: Int(durationDays),
|
||||||
timeOfDay: timeOfDay,
|
timeOfDay: timeOfDay,
|
||||||
iconName: iconName,
|
iconName: iconName,
|
||||||
category: effectiveCategory
|
category: selectedCategory
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update habits - remove old, add new
|
// Update habits - remove old, add new
|
||||||
@ -432,7 +450,7 @@ struct RitualEditSheet: View {
|
|||||||
durationDays: Int(durationDays),
|
durationDays: Int(durationDays),
|
||||||
timeOfDay: timeOfDay,
|
timeOfDay: timeOfDay,
|
||||||
iconName: iconName,
|
iconName: iconName,
|
||||||
category: effectiveCategory,
|
category: selectedCategory,
|
||||||
habits: newHabits
|
habits: newHabits
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -661,5 +679,5 @@ struct HabitIconPickerSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
RitualEditSheet(store: RitualStore.preview, ritual: nil)
|
RitualEditSheet(store: RitualStore.preview, categoryStore: CategoryStore.preview, ritual: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import Sherpa
|
|||||||
struct RootView: View {
|
struct RootView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
@Bindable var settingsStore: SettingsStore
|
@Bindable var settingsStore: SettingsStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@State private var selectedTab: RootTab = .today
|
@State private var selectedTab: RootTab = .today
|
||||||
@ -21,13 +22,13 @@ struct RootView: View {
|
|||||||
TabView(selection: $selectedTab) {
|
TabView(selection: $selectedTab) {
|
||||||
Tab(String(localized: "Today"), systemImage: "sun.max.fill", value: RootTab.today) {
|
Tab(String(localized: "Today"), systemImage: "sun.max.fill", value: RootTab.today) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
TodayView(store: store)
|
TodayView(store: store, categoryStore: categoryStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab(String(localized: "Rituals"), systemImage: "sparkles", value: RootTab.rituals) {
|
Tab(String(localized: "Rituals"), systemImage: "sparkles", value: RootTab.rituals) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RitualsView(store: store)
|
RitualsView(store: store, categoryStore: categoryStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ struct RootView: View {
|
|||||||
|
|
||||||
Tab(String(localized: "Settings"), systemImage: "gearshape.fill", value: RootTab.settings) {
|
Tab(String(localized: "Settings"), systemImage: "gearshape.fill", value: RootTab.settings) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
SettingsView(store: settingsStore, ritualStore: store)
|
SettingsView(store: settingsStore, ritualStore: store, categoryStore: categoryStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +78,7 @@ struct RootView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
RootView(store: RitualStore.preview, settingsStore: SettingsStore.preview)
|
RootView(store: RitualStore.preview, settingsStore: SettingsStore.preview, categoryStore: CategoryStore.preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RootView: SherpaDelegate {
|
extension RootView: SherpaDelegate {
|
||||||
|
|||||||
156
Andromida/App/Views/Settings/CategoryEditSheet.swift
Normal file
156
Andromida/App/Views/Settings/CategoryEditSheet.swift
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
/// Sheet for creating or editing a category
|
||||||
|
struct CategoryEditSheet: View {
|
||||||
|
@Bindable var store: CategoryStore
|
||||||
|
let category: Category?
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State private var name: String = ""
|
||||||
|
@State private var selectedColorName: String = "gray"
|
||||||
|
@State private var showingDeleteConfirmation = false
|
||||||
|
|
||||||
|
private var isEditing: Bool { category != nil }
|
||||||
|
private var isPreset: Bool { category?.isPreset ?? false }
|
||||||
|
|
||||||
|
private var canSave: Bool {
|
||||||
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if isPreset {
|
||||||
|
// Presets can always save (just color change)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// New/user categories need a valid unique name
|
||||||
|
return !trimmedName.isEmpty && store.isNameAvailable(trimmedName, excluding: category)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
if isPreset {
|
||||||
|
// Preset name is read-only
|
||||||
|
HStack {
|
||||||
|
Text(String(localized: "Name"))
|
||||||
|
Spacer()
|
||||||
|
Text(name)
|
||||||
|
.foregroundStyle(AppTextColors.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextField(String(localized: "Category Name"), text: $name)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(String(localized: "Name"))
|
||||||
|
}
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 44))], spacing: Design.Spacing.medium) {
|
||||||
|
ForEach(Category.availableColors, id: \.name) { colorOption in
|
||||||
|
Button {
|
||||||
|
selectedColorName = colorOption.name
|
||||||
|
} label: {
|
||||||
|
Circle()
|
||||||
|
.fill(colorOption.color)
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.overlay(
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(Color.white, lineWidth: selectedColorName == colorOption.name ? 3 : 0)
|
||||||
|
)
|
||||||
|
.shadow(color: selectedColorName == colorOption.name ? colorOption.color.opacity(0.5) : .clear, radius: 4)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.small)
|
||||||
|
} header: {
|
||||||
|
Text(String(localized: "Color"))
|
||||||
|
}
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
|
// Delete button for user categories
|
||||||
|
if isEditing && !isPreset {
|
||||||
|
Section {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
showingDeleteConfirmation = true
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(String(localized: "Delete Category"))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(AppSurface.primary)
|
||||||
|
.navigationTitle(isEditing ? String(localized: "Edit Category") : String(localized: "New Category"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button(String(localized: "Cancel")) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button(String(localized: "Save")) {
|
||||||
|
save()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.disabled(!canSave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadCategory()
|
||||||
|
}
|
||||||
|
.alert(String(localized: "Delete Category?"), isPresented: $showingDeleteConfirmation) {
|
||||||
|
Button(String(localized: "Cancel"), role: .cancel) {}
|
||||||
|
Button(String(localized: "Delete"), role: .destructive) {
|
||||||
|
if let category {
|
||||||
|
store.deleteCategory(category)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(String(localized: "Rituals using this category will be set to no category."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.presentationDetents([.medium])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadCategory() {
|
||||||
|
if let category {
|
||||||
|
name = category.name
|
||||||
|
selectedColorName = category.colorName
|
||||||
|
} else {
|
||||||
|
name = ""
|
||||||
|
selectedColorName = "blue" // Default for new categories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save() {
|
||||||
|
if let category {
|
||||||
|
// Update existing
|
||||||
|
if isPreset {
|
||||||
|
store.updateCategory(category, colorName: selectedColorName)
|
||||||
|
} else {
|
||||||
|
store.updateCategory(category, name: name, colorName: selectedColorName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
store.addCategory(name: name, colorName: selectedColorName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("New Category") {
|
||||||
|
CategoryEditSheet(store: CategoryStore.preview, category: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Edit Preset") {
|
||||||
|
CategoryEditSheet(store: CategoryStore.preview, category: Category.defaultPresets.first!)
|
||||||
|
}
|
||||||
132
Andromida/App/Views/Settings/CategoryListView.swift
Normal file
132
Andromida/App/Views/Settings/CategoryListView.swift
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Bedrock
|
||||||
|
|
||||||
|
/// View for managing categories - presets and user-created
|
||||||
|
struct CategoryListView: View {
|
||||||
|
@Bindable var store: CategoryStore
|
||||||
|
|
||||||
|
@State private var showingAddSheet = false
|
||||||
|
@State private var categoryToEdit: Category?
|
||||||
|
@State private var categoryToDelete: Category?
|
||||||
|
|
||||||
|
private var presetCategories: [Category] {
|
||||||
|
store.categories.filter { $0.isPreset }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var userCategories: [Category] {
|
||||||
|
store.categories.filter { !$0.isPreset }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
// Preset categories section
|
||||||
|
Section {
|
||||||
|
ForEach(presetCategories) { category in
|
||||||
|
categoryRow(category)
|
||||||
|
.onTapGesture {
|
||||||
|
categoryToEdit = category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(String(localized: "Default Categories"))
|
||||||
|
} footer: {
|
||||||
|
Text(String(localized: "Default categories cannot be renamed or deleted, but you can change their color."))
|
||||||
|
}
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
|
||||||
|
// User categories section
|
||||||
|
Section {
|
||||||
|
if userCategories.isEmpty {
|
||||||
|
Text(String(localized: "No custom categories yet"))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
|
} else {
|
||||||
|
ForEach(userCategories) { category in
|
||||||
|
categoryRow(category)
|
||||||
|
.onTapGesture {
|
||||||
|
categoryToEdit = category
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
categoryToDelete = category
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: "Delete"), systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(String(localized: "Custom Categories"))
|
||||||
|
}
|
||||||
|
.listRowBackground(AppSurface.card)
|
||||||
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(AppSurface.primary)
|
||||||
|
.navigationTitle(String(localized: "Categories"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button {
|
||||||
|
showingAddSheet = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.foregroundStyle(AppAccent.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingAddSheet) {
|
||||||
|
CategoryEditSheet(store: store, category: nil)
|
||||||
|
}
|
||||||
|
.sheet(item: $categoryToEdit) { category in
|
||||||
|
CategoryEditSheet(store: store, category: category)
|
||||||
|
}
|
||||||
|
.alert(String(localized: "Delete Category?"), isPresented: .init(
|
||||||
|
get: { categoryToDelete != nil },
|
||||||
|
set: { if !$0 { categoryToDelete = nil } }
|
||||||
|
)) {
|
||||||
|
Button(String(localized: "Cancel"), role: .cancel) {
|
||||||
|
categoryToDelete = nil
|
||||||
|
}
|
||||||
|
Button(String(localized: "Delete"), role: .destructive) {
|
||||||
|
if let category = categoryToDelete {
|
||||||
|
store.deleteCategory(category)
|
||||||
|
}
|
||||||
|
categoryToDelete = nil
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(String(localized: "Rituals using this category will be set to no category."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func categoryRow(_ category: Category) -> some View {
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
Circle()
|
||||||
|
.fill(category.color)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
|
||||||
|
Text(category.name)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if category.isPreset {
|
||||||
|
Text(String(localized: "Default"))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppTextColors.tertiary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
CategoryListView(store: CategoryStore.preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import Bedrock
|
|||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Bindable var store: SettingsStore
|
@Bindable var store: SettingsStore
|
||||||
var ritualStore: RitualStore?
|
var ritualStore: RitualStore?
|
||||||
|
var categoryStore: CategoryStore?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
@ -37,6 +38,24 @@ struct SettingsView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsSectionHeader(
|
||||||
|
title: String(localized: "Customization"),
|
||||||
|
systemImage: "paintbrush",
|
||||||
|
accentColor: AppAccent.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
|
if let categoryStore {
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title: String(localized: "Categories"),
|
||||||
|
subtitle: String(localized: "Manage ritual categories"),
|
||||||
|
backgroundColor: AppSurface.primary
|
||||||
|
) {
|
||||||
|
CategoryListView(store: categoryStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: String(localized: "iCloud Sync"),
|
title: String(localized: "iCloud Sync"),
|
||||||
systemImage: "icloud",
|
systemImage: "icloud",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Bedrock
|
|||||||
|
|
||||||
struct TodayEmptyStateView: View {
|
struct TodayEmptyStateView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
@State private var showingPresetLibrary = false
|
@State private var showingPresetLibrary = false
|
||||||
@State private var showingCreateRitual = false
|
@State private var showingCreateRitual = false
|
||||||
|
|
||||||
@ -70,13 +71,13 @@ struct TodayEmptyStateView: View {
|
|||||||
PresetLibrarySheet(store: store)
|
PresetLibrarySheet(store: store)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingCreateRitual) {
|
.sheet(isPresented: $showingCreateRitual) {
|
||||||
RitualEditSheet(store: store, ritual: nil)
|
RitualEditSheet(store: store, categoryStore: categoryStore, ritual: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
TodayEmptyStateView(store: RitualStore.preview)
|
TodayEmptyStateView(store: RitualStore.preview, categoryStore: CategoryStore.preview)
|
||||||
.padding()
|
.padding()
|
||||||
.background(AppSurface.primary)
|
.background(AppSurface.primary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Bedrock
|
|||||||
|
|
||||||
struct TodayView: View {
|
struct TodayView: View {
|
||||||
@Bindable var store: RitualStore
|
@Bindable var store: RitualStore
|
||||||
|
@Bindable var categoryStore: CategoryStore
|
||||||
|
|
||||||
/// Rituals to show now based on current time of day
|
/// Rituals to show now based on current time of day
|
||||||
private var todayRituals: [Ritual] {
|
private var todayRituals: [Ritual] {
|
||||||
@ -30,7 +31,7 @@ struct TodayView: View {
|
|||||||
TodayNoRitualsForTimeView(store: store)
|
TodayNoRitualsForTimeView(store: store)
|
||||||
} else {
|
} else {
|
||||||
// No active rituals at all
|
// No active rituals at all
|
||||||
TodayEmptyStateView(store: store)
|
TodayEmptyStateView(store: store, categoryStore: categoryStore)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ForEach(todayRituals) { ritual in
|
ForEach(todayRituals) { ritual in
|
||||||
@ -62,7 +63,7 @@ struct TodayView: View {
|
|||||||
set: { if !$0 { store.dismissRenewalPrompt() } }
|
set: { if !$0 { store.dismissRenewalPrompt() } }
|
||||||
)) {
|
)) {
|
||||||
if let ritual = store.ritualNeedingRenewal {
|
if let ritual = store.ritualNeedingRenewal {
|
||||||
ArcRenewalSheet(store: store, ritual: ritual)
|
ArcRenewalSheet(store: store, categoryStore: categoryStore, ritual: ritual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,5 +82,5 @@ struct TodayView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
TodayView(store: RitualStore.preview)
|
TodayView(store: RitualStore.preview, categoryStore: CategoryStore.preview)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user