// // ContentView.swift // Keging // // 첫 화면 — 큰 시작 버튼 + 구석의 세팅·설정 버튼. 아래로 스와이프하면 통계. // import SwiftUI struct ContentView: View { @EnvironmentObject private var engine: KegelEngine @EnvironmentObject private var settings: SettingsStore @State private var showTimerSettings = false @State private var showAppSettings = false @State private var showStats = false var body: some View { ZStack { AppTheme.background.ignoresSafeArea() mainScreen if showStats { StatsView(onClose: closeStats) .transition(.move(edge: .top)) .zIndex(2) } } .sheet(isPresented: $showTimerSettings) { TimerSettingsView() } .sheet(isPresented: $showAppSettings) { AppSettingsView() } .fullScreenCover(isPresented: workoutPresented) { WorkoutView() } .onReceive(NotificationCenter.default.publisher(for: .startWorkoutRequested)) { _ in startFromShortcut() } .onAppear { if AppLaunchState.shared.startRequested { startFromShortcut() } #if DEBUG if CommandLine.arguments.contains("-openStats") { showStats = true } if CommandLine.arguments.contains("-autoStart") { WorkoutCoordinator.shared.startWorkout() } #endif } } // MARK: 첫 화면 private var mainScreen: some View { VStack(spacing: 0) { HStack(alignment: .top) { cornerButton(symbol: "gearshape.fill", label: "설정") { showAppSettings = true } Spacer() Button { openStats() } label: { VStack(spacing: 2) { Capsule().fill(.secondary.opacity(0.5)).frame(width: 36, height: 5) Image(systemName: "chevron.compact.down") .font(.body) .foregroundStyle(.secondary) } .padding(.top, 10) } .buttonStyle(.plain) .accessibilityLabel("통계 보기") Spacer() cornerButton(symbol: "slider.horizontal.3", label: "운동 세팅") { showTimerSettings = true } } .padding(.horizontal, 20) .padding(.top, 8) Spacer() Button { WorkoutCoordinator.shared.startWorkout() } label: { startButtonLabel } .buttonStyle(.plain) .accessibilityLabel("케겔 운동 시작") Button { showTimerSettings = true } label: { Text(settings.config.summaryText) .font(.subheadline) .foregroundStyle(.secondary) } .buttonStyle(.plain) .padding(.top, 30) .accessibilityLabel("운동 세팅 열기 — 현재 \(settings.config.summaryText)") Spacer() Spacer() } .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 25) .onEnded { value in if value.translation.height > 60, abs(value.translation.width) < 100 { openStats() } } ) } private var startButtonLabel: some View { ZStack { Circle() .fill( RadialGradient( colors: [AppTheme.green.opacity(0.22), .clear], center: .center, startRadius: 100, endRadius: 165 ) ) .frame(width: 330, height: 330) Circle() .fill( LinearGradient( colors: [AppTheme.green, AppTheme.green.opacity(0.82)], startPoint: .top, endPoint: .bottom ) ) .frame(width: 236, height: 236) .shadow(color: AppTheme.green.opacity(0.35), radius: 24, x: 0, y: 10) VStack(spacing: 8) { Image(systemName: "play.fill").font(.system(size: 40, weight: .bold)) Text("시작").font(.title.bold()) } .foregroundStyle(startButtonForeground) } .frame(width: 330, height: 330) } /// 다크의 세이지 민트 버튼 위에는 흰 글자가 묻혀서 짙은 그린 틴트 글자로 private var startButtonForeground: Color { Color(light: .white, dark: Color(hex: "#0F2018")) } private func cornerButton(symbol: String, label: String, action: @escaping () -> Void) -> some View { Button(action: action) { Image(systemName: symbol) .font(.title3) .foregroundStyle(.secondary) .frame(width: 44, height: 44) .background(AppTheme.surface, in: Circle()) } .buttonStyle(.plain) .accessibilityLabel(label) } // MARK: 통계·운동 전환 private func openStats() { withAnimation(.spring(duration: 0.35)) { showStats = true } } private func closeStats() { withAnimation(.spring(duration: 0.35)) { showStats = false } } private var workoutPresented: Binding { Binding( get: { engine.state != .idle }, set: { presented in if !presented { engine.acknowledgeFinish() } } ) } private func startFromShortcut() { AppLaunchState.shared.startRequested = false showStats = false WorkoutCoordinator.shared.startWorkout() } }