68fed087-9049-446f-bfc5-3f8.../Sources/ui/WaterView.swift

319 lines
11 KiB
Swift
Raw Normal View History

//
// WaterView.swift
// plugin v0.1
//
// 8 cup target with progress dots, tap big cup to log a sip.
//
import SwiftUI
struct WaterView: View {
@ObservedObject var store: WorkerStore
var body: some View {
ScrollView {
VStack(spacing: 14) {
statusBadge
cupHero
controls
Divider()
.background(WorkerTheme.overlay08)
.padding(.horizontal, 24)
goalRow
toggleRows
tipText
Spacer(minLength: 0)
}
.padding(.top, 8)
.padding(.bottom, 8)
}
}
// MARK: - Toggle rows (auto-from-pomodoro + hourly reminder)
private var toggleRows: some View {
VStack(spacing: 8) {
togglePill(
icon: "timer",
label: "番茄完成 +1 杯",
isOn: store.waterAutoFromPomodoro,
tint: WorkerTheme.tomato,
action: { store.waterSetAutoFromPomodoro(!store.waterAutoFromPomodoro) }
)
togglePill(
icon: "bell.fill",
label: "整点提醒918 点)",
isOn: store.waterHourlyReminder,
tint: WorkerTheme.water,
action: { store.waterSetHourlyReminder(!store.waterHourlyReminder) }
)
}
.padding(.horizontal, 16)
}
private func togglePill(icon: String, label: String, isOn: Bool, tint: Color, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 11, weight: .semibold))
.foregroundColor(isOn ? tint : WorkerTheme.fg55)
.frame(width: 18)
Text(label)
.font(.system(size: 11.5, weight: .medium))
.foregroundColor(isOn ? WorkerTheme.fgPrimary : WorkerTheme.fg70)
Spacer()
// Slim track-style toggle indicator.
ZStack(alignment: isOn ? .trailing : .leading) {
Capsule()
.fill(isOn ? tint.opacity(0.35) : WorkerTheme.overlay08)
.frame(width: 28, height: 16)
Circle()
.fill(isOn ? tint : WorkerTheme.fg55)
.frame(width: 12, height: 12)
.padding(2)
}
}
.padding(.horizontal, 12)
.frame(height: 32)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(WorkerTheme.overlay04)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(isOn ? tint.opacity(0.35) : WorkerTheme.overlay08, lineWidth: 0.5)
)
)
}
.buttonStyle(.plain)
}
// MARK: - Status
private var statusBadge: some View {
HStack(spacing: 8) {
Circle()
.fill(WorkerTheme.water)
.frame(width: 8, height: 8)
.shadow(color: WorkerTheme.water.opacity(0.7), radius: 4)
Text(badgeText)
.font(.system(size: 12.5, weight: .semibold))
.foregroundColor(WorkerTheme.fg85)
}
.padding(.horizontal, 12)
.frame(height: 26)
.background(Capsule().fill(WorkerTheme.overlay06))
}
private var badgeText: String {
if store.waterCupsToday >= store.waterGoal {
return "今日目标已达成 ✨"
}
return "今日 \(store.waterCupsToday) / \(store.waterGoal)"
}
// MARK: - Cup hero (tappable)
private var cupHero: some View {
VStack(spacing: 12) {
Button(action: { store.waterAddCup() }) {
ZStack {
// Water-fill cup illustration.
cupShape
.frame(width: 130, height: 160)
}
}
.buttonStyle(.plain)
.help("点击杯子记录一杯水")
// Progress dots
HStack(spacing: 6) {
ForEach(0..<store.waterGoal, id: \.self) { i in
Circle()
.fill(i < store.waterCupsToday ? WorkerTheme.water : WorkerTheme.overlay08)
.frame(width: 9, height: 9)
.overlay(
Circle()
.stroke(WorkerTheme.overlay12, lineWidth: 0.5)
)
}
}
}
}
private var cupShape: some View {
ZStack(alignment: .bottom) {
// Outline (cup body slight trapezoid)
CupOutline()
.stroke(WorkerTheme.fg55, lineWidth: 2.5)
// Water fill (clipped to the cup)
CupOutline()
.fill(LinearGradient(
colors: [WorkerTheme.water.opacity(0.85), WorkerTheme.water.opacity(0.4)],
startPoint: .top, endPoint: .bottom
))
.mask(
GeometryReader { geo in
let frac = min(1.0, Double(store.waterCupsToday) / Double(max(1, store.waterGoal)))
Rectangle()
.frame(width: geo.size.width, height: geo.size.height * CGFloat(frac))
.frame(maxHeight: .infinity, alignment: .bottom)
}
)
.animation(.easeInOut(duration: 0.4), value: store.waterCupsToday)
// Big number on top of the cup
VStack {
Spacer()
Text("\(store.waterCupsToday)")
.font(.system(size: 40, weight: .semibold, design: .rounded))
.foregroundColor(WorkerTheme.fgPrimary)
.monospacedDigit()
Text("")
.font(.system(size: 11))
.foregroundColor(WorkerTheme.fg55)
.padding(.bottom, 18)
}
}
}
// MARK: - Controls
private var controls: some View {
HStack(spacing: 10) {
Button(action: { store.waterAddCup() }) {
HStack(spacing: 6) {
Image(systemName: "plus")
.font(.system(size: 11, weight: .semibold))
Text("加一杯")
.font(.system(size: 12.5, weight: .semibold))
}
.foregroundColor(Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255))
.padding(.horizontal, 18)
.frame(height: 32)
.background(Capsule().fill(WorkerTheme.lime))
}
.buttonStyle(.plain)
Button(action: { store.waterRemoveCup() }) {
HStack(spacing: 6) {
Image(systemName: "minus")
.font(.system(size: 11, weight: .semibold))
Text("撤销")
.font(.system(size: 12.5, weight: .semibold))
}
.foregroundColor(WorkerTheme.fg85)
.padding(.horizontal, 16)
.frame(height: 32)
.background(
Capsule()
.fill(WorkerTheme.overlay06)
.overlay(Capsule().stroke(WorkerTheme.overlay12, lineWidth: 0.5))
)
}
.buttonStyle(.plain)
}
}
// MARK: - Goal row
private var goalRow: some View {
HStack(spacing: 0) {
Text("每日目标")
.font(.system(size: 11))
.foregroundColor(WorkerTheme.fg55)
.padding(.leading, 12)
Spacer()
Button(action: { store.waterSetGoal(store.waterGoal - 1) }) {
Image(systemName: "minus")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(WorkerTheme.fg70)
.frame(width: 24, height: 24)
.background(Circle().fill(WorkerTheme.overlay06))
}
.buttonStyle(.plain)
Text("\(store.waterGoal)")
.font(.system(size: 13, weight: .semibold))
.foregroundColor(WorkerTheme.fgPrimary)
.monospacedDigit()
.frame(minWidth: 50)
Button(action: { store.waterSetGoal(store.waterGoal + 1) }) {
Image(systemName: "plus")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(WorkerTheme.fg70)
.frame(width: 24, height: 24)
.background(Circle().fill(WorkerTheme.overlay06))
}
.buttonStyle(.plain)
.padding(.trailing, 8)
}
.frame(height: 36)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(WorkerTheme.overlay04)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(WorkerTheme.overlay08, lineWidth: 0.5)
)
)
.padding(.horizontal, 16)
}
private var tipText: some View {
Text(store.waterCupsToday >= store.waterGoal
? "今天的水喝够了,给自己鼓个掌 👏"
: "保持每小时一杯,工作更高效。")
.font(.system(size: 11))
.foregroundColor(WorkerTheme.fg45)
.padding(.horizontal, 20)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
}
}
// MARK: - Cup outline shape
private struct CupOutline: Shape {
func path(in rect: CGRect) -> Path {
// Slight trapezoid: narrower at the bottom. Straight rim with
// a 4pt rounded base.
var p = Path()
let topInset: CGFloat = 0
let bottomInset: CGFloat = rect.width * 0.08
let radius: CGFloat = 8
// Start top-left.
p.move(to: CGPoint(x: rect.minX + topInset, y: rect.minY))
// Top edge.
p.addLine(to: CGPoint(x: rect.maxX - topInset, y: rect.minY))
// Right side (sloping inward).
p.addLine(to: CGPoint(x: rect.maxX - bottomInset, y: rect.maxY - radius))
// Bottom-right curve.
p.addQuadCurve(
to: CGPoint(x: rect.maxX - bottomInset - radius, y: rect.maxY),
control: CGPoint(x: rect.maxX - bottomInset, y: rect.maxY)
)
// Bottom edge.
p.addLine(to: CGPoint(x: rect.minX + bottomInset + radius, y: rect.maxY))
// Bottom-left curve.
p.addQuadCurve(
to: CGPoint(x: rect.minX + bottomInset, y: rect.maxY - radius),
control: CGPoint(x: rect.minX + bottomInset, y: rect.maxY)
)
// Left side back to top.
p.addLine(to: CGPoint(x: rect.minX + topInset, y: rect.minY))
p.closeSubpath()
return p
}
}