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

161 lines
5.0 KiB
Swift

import SwiftUI
import SwiftData
struct DashboardView: View {
@Environment(\.modelContext) private var modelContext
@State private var viewModel = DashboardViewModel()
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
progressRing
if !viewModel.topGoalProgress.isEmpty {
topGoals
}
remainingTasksSection
}
.padding()
}
}
.navigationTitle(String(localized: "tab.dashboard"))
.onAppear { viewModel.setup(context: modelContext) }
}
// MARK: - Progress Ring
private var progressRing: some View {
VStack(spacing: 12) {
ZStack {
Circle()
.stroke(Theme.surface, lineWidth: 16)
Circle()
.trim(from: 0, to: viewModel.overallProgress)
.stroke(Theme.green, style: StrokeStyle(lineWidth: 16, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.easeInOut(duration: 0.4), value: viewModel.overallProgress)
VStack(spacing: 4) {
Text("\(Int(viewModel.overallProgress * 100))%")
.font(.system(size: 34, weight: .bold, design: .rounded))
.foregroundStyle(Theme.primaryText)
Text(String(localized: "dashboard.progress.title"))
.font(.caption)
.foregroundStyle(Theme.primaryText.opacity(0.6))
}
}
.frame(width: 180, height: 180)
.padding(.top, 8)
}
}
// MARK: - Top Goals
private var topGoals: some View {
VStack(alignment: .leading, spacing: 10) {
Text(String(localized: "dashboard.goals.title"))
.font(.headline)
.foregroundStyle(Theme.primaryText)
VStack(spacing: 10) {
ForEach(viewModel.topGoalProgress) { progress in
TopGoalCard(progress: progress)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
// MARK: - Remaining Tasks
private var remainingTasksSection: some View {
VStack(alignment: .leading, spacing: 10) {
Text(String(localized: "dashboard.tasks.title"))
.font(.headline)
.foregroundStyle(Theme.primaryText)
if viewModel.remainingTasks.isEmpty {
Text(String(localized: "dashboard.tasks.empty"))
.font(.subheadline)
.foregroundStyle(Theme.primaryText.opacity(0.5))
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 24)
} else {
VStack(spacing: 8) {
ForEach(viewModel.remainingTasks) { task in
RemainingTaskRow(task: task) {
viewModel.addCount(to: task)
}
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
// MARK: - Top Goal Card
private struct TopGoalCard: View {
let progress: GoalProgress
var body: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text(progress.displayName)
.font(.body)
.fontWeight(.semibold)
.foregroundStyle(Theme.primaryText)
ProgressView(value: progress.ratio)
.tint(progress.isCompleted ? Theme.green : Theme.yellow)
}
Text(progress.percentText)
.font(.caption)
.fontWeight(.bold)
.foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow)
}
.padding()
.background(Theme.surface, in: RoundedRectangle(cornerRadius: 12))
}
}
// MARK: - Remaining Task Row
private struct RemainingTaskRow: View {
let task: TaskItem
let onAddCount: () -> Void
var body: some View {
HStack(spacing: 12) {
Text(task.icon)
.font(.title3)
.frame(width: 30)
Text(task.name)
.font(.body)
.foregroundStyle(Theme.primaryText)
Spacer()
if task.type == .count {
Button(action: onAddCount) {
Image(systemName: "plus.circle.fill")
.foregroundStyle(Theme.green)
}
.buttonStyle(.plain)
} else {
Image(systemName: "clock")
.foregroundStyle(Theme.primaryText.opacity(0.4))
}
}
.padding(12)
.background(Theme.surface, in: RoundedRectangle(cornerRadius: 10))
}
}
#Preview {
NavigationStack {
DashboardView()
}
}