178 lines
6.1 KiB
Swift
178 lines
6.1 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct CategoryView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@State private var viewModel = CategoryViewModel()
|
|
@State private var showAddSheet = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Theme.background.ignoresSafeArea()
|
|
Group {
|
|
if viewModel.categories.isEmpty {
|
|
emptyState
|
|
} else {
|
|
categoryList
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "tab.category"))
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button { showAddSheet = true } label: {
|
|
Image(systemName: "plus")
|
|
.foregroundStyle(Theme.green)
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showAddSheet) {
|
|
AddCategorySheet { name, colorHex in
|
|
viewModel.addCategory(name: name, colorHex: colorHex)
|
|
}
|
|
}
|
|
.onAppear { viewModel.setup(context: modelContext) }
|
|
}
|
|
|
|
private var emptyState: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "folder.badge.plus")
|
|
.font(.system(size: 56))
|
|
.foregroundStyle(Theme.green.opacity(0.6))
|
|
Text(String(localized: "category.empty"))
|
|
.foregroundStyle(Theme.primaryText.opacity(0.6))
|
|
}
|
|
}
|
|
|
|
private var categoryList: some View {
|
|
List {
|
|
ForEach(viewModel.categories) { category in
|
|
CategoryRow(category: category, taskCount: viewModel.taskCount(for: category))
|
|
.listRowBackground(Theme.surface)
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
Button(role: .destructive) {
|
|
viewModel.deleteCategory(category)
|
|
} label: {
|
|
Label(String(localized: "category.delete"), systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.scrollContentBackground(.hidden)
|
|
}
|
|
}
|
|
|
|
// MARK: - Category Row
|
|
|
|
private struct CategoryRow: View {
|
|
let category: Category
|
|
let taskCount: Int
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Circle()
|
|
.fill(Color(hex: category.colorHex))
|
|
.frame(width: 14, height: 14)
|
|
|
|
Text(category.name)
|
|
.font(.body)
|
|
.fontWeight(.medium)
|
|
.foregroundStyle(Theme.primaryText)
|
|
|
|
Spacer()
|
|
|
|
Text("\(taskCount) \(String(localized: "category.task.count"))")
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.primaryText.opacity(0.5))
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Theme.green.opacity(0.12), in: Capsule())
|
|
}
|
|
.padding(.vertical, 6)
|
|
}
|
|
}
|
|
|
|
// MARK: - Add Category Sheet
|
|
|
|
private struct AddCategorySheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var name = ""
|
|
@State private var selectedColor = CategoryViewModel.presetColors[0]
|
|
|
|
let onAdd: (String, String) -> Void
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
TextField(String(localized: "category.name"), text: $name)
|
|
}
|
|
|
|
Section(String(localized: "category.color")) {
|
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) {
|
|
ForEach(CategoryViewModel.presetColors, id: \.self) { colorHex in
|
|
Circle()
|
|
.fill(Color(hex: colorHex))
|
|
.frame(width: 42, height: 42)
|
|
.overlay(alignment: .center) {
|
|
if selectedColor == colorHex {
|
|
Image(systemName: "checkmark")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.foregroundStyle(.white)
|
|
}
|
|
}
|
|
.overlay(
|
|
Circle()
|
|
.strokeBorder(
|
|
selectedColor == colorHex ? Theme.primaryText.opacity(0.4) : Color.clear,
|
|
lineWidth: 2
|
|
)
|
|
)
|
|
.onTapGesture { selectedColor = colorHex }
|
|
}
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "category.add"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: "button.cancel")) { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String(localized: "button.add")) {
|
|
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
|
guard !trimmed.isEmpty else { return }
|
|
onAdd(trimmed, selectedColor)
|
|
dismiss()
|
|
}
|
|
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Color Hex Extension
|
|
|
|
extension Color {
|
|
init(hex: String) {
|
|
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
|
var int: UInt64 = 0
|
|
Scanner(string: hex).scanHexInt64(&int)
|
|
let r = Double((int >> 16) & 0xFF) / 255
|
|
let g = Double((int >> 8) & 0xFF) / 255
|
|
let b = Double(int & 0xFF) / 255
|
|
self.init(red: r, green: g, blue: b)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
CategoryView()
|
|
}
|
|
}
|