// ── Pomodoro View ────────────────────────────────────────────── // Focus session with countdown ring, round tracking, and auto-transitions import SwiftUI struct PomodoroView: View { @EnvironmentObject var store: TimerStore @State private var showCreatePomodoro = false private var activePomodoro: CMTimer? { store.timers.first { $0.type == .pomodoro && [.active, .warning, .paused, .firing].contains($0.state) } } var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() if let timer = activePomodoro { ActivePomodoroView(timer: timer) } else { IdlePomodoroView(onStart: { showCreatePomodoro = true }) } } .navigationTitle("Focus") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(CMColors.surface, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) .sheet(isPresented: $showCreatePomodoro) { PomodoroSetupSheet() } } } } // MARK: - Idle State struct IdlePomodoroView: View { let onStart: () -> Void @EnvironmentObject var store: TimerStore var body: some View { VStack(spacing: CMSpacing.xxl) { Spacer() // Decorative ring ZStack { Circle() .stroke(CMColors.border, lineWidth: 12) .frame(width: 220, height: 220) VStack(spacing: CMSpacing.sm) { Image(systemName: "target") .font(.system(size: 40)) .foregroundStyle(CMColors.accent) Text("Ready to focus?") .font(CMFonts.body(size: 18, weight: .semibold)) .foregroundStyle(CMColors.text) } } Text("Start a Pomodoro session to stay focused\nwith structured work and break intervals") .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textMuted) .multilineTextAlignment(.center) // Quick start buttons VStack(spacing: CMSpacing.md) { Button { HapticEngine.tap() let _ = store.addPomodoro() } label: { HStack { Image(systemName: "play.fill") Text("Start 25/5 Session") } .font(CMFonts.body(size: 16, weight: .semibold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, CMSpacing.lg) .background(CMColors.accent) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) } Button { onStart() } label: { HStack { Image(systemName: "slider.horizontal.3") Text("Custom Session") } .font(CMFonts.body(size: 16, weight: .medium)) .foregroundStyle(CMColors.accent) .frame(maxWidth: .infinity) .padding(.vertical, CMSpacing.lg) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) .overlay( RoundedRectangle(cornerRadius: CMRadius.md) .stroke(CMColors.accent.opacity(0.3), lineWidth: 1) ) } } .padding(.horizontal, CMSpacing.xxl) Spacer() } } } // MARK: - Active Pomodoro struct ActivePomodoroView: View { let timer: CMTimer @EnvironmentObject var store: TimerStore private var progress: Double { guard let duration = timer.duration, duration > 0 else { return 0 } let remaining = getRemainingSeconds(timer, now: store.now) return 1.0 - (remaining / duration) } private var phaseLabel: String { guard let state = timer.pomodoroState else { return "" } if state.isLongBreak { return "Long Break" } if state.isBreak { return "Break" } return "Work" } private var phaseIcon: String { guard let state = timer.pomodoroState else { return "target" } if state.isBreak || state.isLongBreak { return "cup.and.saucer.fill" } return "brain.head.profile" } var body: some View { VStack(spacing: CMSpacing.xl) { Spacer() // Phase indicator HStack(spacing: CMSpacing.sm) { Image(systemName: phaseIcon) .foregroundStyle(CMColors.accent) Text(phaseLabel) .font(CMFonts.body(size: 16, weight: .semibold)) .foregroundStyle(CMColors.text) } // Countdown ring CountdownRing( progress: progress, urgency: timer.urgency, remainingSeconds: getRemainingSeconds(timer, now: store.now), totalSeconds: timer.duration ?? 0 ) // Timer label Text(timer.label) .font(CMFonts.body(size: 18, weight: .medium)) .foregroundStyle(CMColors.textSecondary) // Round indicator if let state = timer.pomodoroState, let config = timer.pomodoroConfig { PomodoroRoundIndicator( currentRound: state.currentRound, totalRounds: config.rounds, completedRounds: state.completedRounds, isBreak: state.isBreak || state.isLongBreak ) } // Time blindness aid let remaining = getRemainingSeconds(timer, now: store.now) if let ref = getTimeReference(seconds: remaining) { Text(ref) .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) .italic() } Spacer() // Controls HStack(spacing: CMSpacing.xxl) { // Cancel Button { HapticEngine.tap() store.dismiss(timer.id) } label: { VStack(spacing: CMSpacing.xs) { Image(systemName: "xmark") .font(.title3) Text("Cancel") .font(CMFonts.body(size: 12)) } .foregroundStyle(CMColors.textMuted) .frame(width: 70) } // Pause / Resume / Advance if timer.state == .firing { Button { HapticEngine.tap() store.advancePom(timer.id) } label: { VStack(spacing: CMSpacing.xs) { Image(systemName: "forward.fill") .font(.title) Text("Next") .font(CMFonts.body(size: 12)) } .foregroundStyle(.white) .frame(width: 80, height: 80) .background(CMColors.accent) .clipShape(Circle()) } } else if timer.state == .paused { Button { HapticEngine.tap() store.resume(timer.id) } label: { VStack(spacing: CMSpacing.xs) { Image(systemName: "play.fill") .font(.title) Text("Resume") .font(CMFonts.body(size: 12)) } .foregroundStyle(.white) .frame(width: 80, height: 80) .background(CMColors.accent) .clipShape(Circle()) } } else { Button { HapticEngine.tap() store.pause(timer.id) } label: { VStack(spacing: CMSpacing.xs) { Image(systemName: "pause.fill") .font(.title) Text("Pause") .font(CMFonts.body(size: 12)) } .foregroundStyle(.white) .frame(width: 80, height: 80) .background(CMColors.surface) .clipShape(Circle()) .overlay(Circle().stroke(CMColors.border, lineWidth: 2)) } } // Skip to break / work Button { HapticEngine.tap() store.advancePom(timer.id) } label: { VStack(spacing: CMSpacing.xs) { Image(systemName: "forward.end.fill") .font(.title3) Text("Skip") .font(CMFonts.body(size: 12)) } .foregroundStyle(CMColors.textMuted) .frame(width: 70) } } .padding(.bottom, CMSpacing.xxl) } } } // MARK: - Pomodoro Setup Sheet struct PomodoroSetupSheet: View { @EnvironmentObject var store: TimerStore @Environment(\.dismiss) private var dismiss @State private var label = "Focus Session" @State private var workMinutes = 25 @State private var breakMinutes = 5 @State private var longBreakMinutes = 15 @State private var rounds = 4 var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() ScrollView { VStack(spacing: CMSpacing.xl) { CMTextField(title: "Session Label", placeholder: "e.g., Deep work", text: $label) CMStepper(title: "Work", value: $workMinutes, range: 5...90, suffix: "min") CMStepper(title: "Break", value: $breakMinutes, range: 1...30, suffix: "min") CMStepper(title: "Long Break", value: $longBreakMinutes, range: 5...60, suffix: "min") CMStepper(title: "Rounds", value: $rounds, range: 1...12, suffix: "") // Total time let totalMinutes = (workMinutes * rounds) + (breakMinutes * max(0, rounds - 1)) + longBreakMinutes HStack { Text("Total session time:") .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textSecondary) Spacer() Text(formatDurationCompact(TimeInterval(totalMinutes * 60))) .font(CMFonts.mono(size: 16, weight: .semibold)) .foregroundStyle(CMColors.accent) } .padding(CMSpacing.md) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) // Time reference if let ref = getTimeReference(minutes: totalMinutes) { HStack { Image(systemName: "lightbulb.fill") .foregroundStyle(CMColors.standard) .font(.caption) Text(ref) .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) .italic() } } } .padding(CMSpacing.lg) } } .navigationTitle("Pomodoro Setup") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } .foregroundStyle(CMColors.textSecondary) } ToolbarItem(placement: .confirmationAction) { Button("Start") { let config = PomodoroConfig( workMinutes: workMinutes, breakMinutes: breakMinutes, longBreakMinutes: longBreakMinutes, rounds: rounds ) let _ = store.addPomodoro(CreatePomodoroParams( label: label, config: config )) dismiss() } .font(.body.weight(.semibold)) .foregroundStyle(CMColors.accent) } } .toolbarBackground(CMColors.surface, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) } } }