151 lines
5.3 KiB
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
|
|
}
|
|
}
|