Andromida/Andromida/App/State/CategoryStore.swift

151 lines
5.3 KiB
Swift

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
}
}