mycode/myApp/LemonLimeTracker/IOS/Views/TaskTrackerView.swift
2026-06-19 19:53:54 +09:00

213 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import SwiftData
struct TaskTrackerView: View {
@Environment(\.modelContext) private var modelContext
@State private var viewModel = TaskViewModel()
@State private var showAddSheet = false
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
Group {
if viewModel.tasks.isEmpty {
emptyState
} else {
taskList
}
}
}
.navigationTitle(String(localized: "tab.tasks"))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showAddSheet = true
} label: {
Image(systemName: "plus")
.foregroundStyle(Theme.green)
}
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskSheet { name, icon, type, category in
viewModel.addTask(name: name, icon: icon, type: type, category: category)
}
}
.onAppear {
viewModel.setup(context: modelContext)
}
}
private var emptyState: some View {
VStack(spacing: 16) {
Image(systemName: "checkmark.circle.badge.plus")
.font(.system(size: 56))
.foregroundStyle(Theme.green.opacity(0.6))
Text(String(localized: "task.empty"))
.foregroundStyle(Theme.primaryText.opacity(0.6))
}
}
private var taskList: some View {
List {
ForEach(viewModel.tasks) { task in
TaskRow(task: task, viewModel: viewModel)
.listRowBackground(Theme.surface)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
viewModel.deleteTask(task)
} label: {
Label(String(localized: "task.delete"), systemImage: "trash")
}
}
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if task.type == .count {
Button {
viewModel.addCount(to: task)
} label: {
Label(String(localized: "task.add.count"), systemImage: "plus")
}
.tint(Theme.green)
}
}
}
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
}
}
// MARK: - Task Row
private struct TaskRow: View {
let task: TaskItem
let viewModel: TaskViewModel
private var isRunning: Bool { viewModel.isTimerRunning(for: task.id) }
var body: some View {
HStack(spacing: 12) {
Text(task.icon)
.font(.title2)
.frame(width: 36)
VStack(alignment: .leading, spacing: 2) {
Text(task.name)
.font(.body)
.fontWeight(.medium)
.foregroundStyle(Theme.primaryText)
if let category = task.category {
Text(category.name)
.font(.caption2)
.foregroundStyle(Theme.green)
}
}
Spacer()
if task.type == .time {
timeControls
} else {
countBadge
}
}
.padding(.vertical, 4)
}
private var timeControls: some View {
HStack(spacing: 8) {
if isRunning {
Text(viewModel.formattedElapsed(for: task.id))
.font(.system(.subheadline, design: .monospaced))
.foregroundStyle(Theme.green)
.contentTransition(.numericText())
.animation(.linear(duration: 0.3), value: viewModel.elapsedSeconds[task.id])
}
Button {
viewModel.toggleTimer(for: task)
} label: {
Image(systemName: isRunning ? "stop.fill" : "play.fill")
.font(.title3)
.foregroundStyle(isRunning ? Theme.yellow : Theme.green)
.frame(width: 36, height: 36)
.background(
Circle()
.fill(isRunning ? Theme.yellow.opacity(0.15) : Theme.green.opacity(0.15))
)
}
.buttonStyle(.plain)
}
}
private var countBadge: some View {
let count = viewModel.todayCount(for: task)
return Text("×\(count)")
.font(.headline)
.monospacedDigit()
.foregroundStyle(count > 0 ? Theme.green : Theme.primaryText.opacity(0.3))
}
}
// MARK: - Add Task Sheet
private struct AddTaskSheet: View {
@Environment(\.dismiss) private var dismiss
@Query private var categories: [Category]
@State private var name = ""
@State private var icon = ""
@State private var type: TaskType = .count
@State private var selectedCategory: Category?
let onAdd: (String, String, TaskType, Category?) -> Void
var body: some View {
NavigationStack {
Form {
Section {
TextField(String(localized: "task.name"), text: $name)
TextField(String(localized: "task.icon"), text: $icon)
}
Section {
Picker(String(localized: "task.type"), selection: $type) {
Text(String(localized: "task.type.count")).tag(TaskType.count)
Text(String(localized: "task.type.time")).tag(TaskType.time)
}
.pickerStyle(.segmented)
}
if !categories.isEmpty {
Section {
Picker(String(localized: "task.category"), selection: $selectedCategory) {
Text(String(localized: "task.category.none")).tag(Optional<Category>.none)
ForEach(categories) { cat in
Text(cat.name).tag(Optional(cat))
}
}
}
}
}
.navigationTitle(String(localized: "task.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 }
let resolvedIcon = icon.trimmingCharacters(in: .whitespaces).isEmpty ? "" : icon
onAdd(trimmed, resolvedIcon, type, selectedCategory)
dismiss()
}
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
}
}
}
#Preview {
TaskTrackerView()
}