// // ContentView.swift // Keging // // 첫 화면 — 큰 시작 버튼 + 구석의 세팅·설정 버튼. 아래로 스와이프하면 통계. // import SwiftUI struct ContentView: View { @EnvironmentObject private var engine: KegelEngine @EnvironmentObject private var settings: SettingsStore @Environment(\.scenePhase) private var scenePhase @State private var showTimerSettings = false @State private var showAppSettings = false @State private var showHelp = false @State private var showStats = false var body: some View { ZStack { AppTheme.background.ignoresSafeArea() mainScreen if showStats { StatsView(onClose: closeStats) .transition(.move(edge: .bottom)) .zIndex(2) } } .sheet(isPresented: $showTimerSettings) { TimerSettingsView() } .sheet(isPresented: $showAppSettings) { AppSettingsView() } .sheet(isPresented: $showHelp) { HelpView() } .fullScreenCover(isPresented: workoutPresented) { WorkoutView() } .onReceive(NotificationCenter.default.publisher(for: .startWorkoutRequested)) { _ in startFromShortcut() } .onChange(of: scenePhase) { // 백그라운드에서 정지됐다 돌아와도 벽시계 기준 정확한 국면으로 재정렬 if scenePhase == .active { KegelEngine.shared.resyncAfterWake() } } .onAppear { if AppLaunchState.shared.startRequested { startFromShortcut() } #if DEBUG if CommandLine.arguments.contains("-openStats") { showStats = true } if CommandLine.arguments.contains("-openHelp") { showHelp = true } if CommandLine.arguments.contains("-openSettings") { showAppSettings = true } if CommandLine.arguments.contains("-openTimerSettings") { showTimerSettings = 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: Text("설정")) { showAppSettings = true } Spacer() cornerButton(symbol: "slider.horizontal.3", label: Text("운동 세팅")) { showTimerSettings = true } } .padding(.horizontal, 20) .padding(.top, 8) Spacer() Button { WorkoutCoordinator.shared.startWorkout() } label: { startButtonLabel } .buttonStyle(.plain) .accessibilityLabel(Text("케겔 운동 시작")) Button { showTimerSettings = true } label: { HStack(spacing: 12) { summaryChip( symbol: KegelPhase.contract.symbolName, value: String(localized: "\(settings.config.contractSeconds)초"), label: Text("수축"), color: AppTheme.green ) summaryChip( symbol: KegelPhase.relax.symbolName, value: String(localized: "\(settings.config.relaxSeconds)초"), label: Text("이완"), color: AppTheme.yellow ) summaryChip( symbol: "repeat", value: String(localized: "\(settings.config.reps)회"), label: Text("횟수"), color: AppTheme.green ) } } .buttonStyle(.plain) .padding(.top, 34) .accessibilityElement(children: .ignore) .accessibilityLabel(Text("운동 세팅 열기 — 현재 \(settings.config.summaryText)")) Spacer() Spacer() // 하단: 가운데 = 통계 손잡이(위로 스와이프 힌트), 오른쪽 = 도움말 ZStack { Button { openStats() } label: { VStack(spacing: 2) { Image(systemName: "chevron.compact.up") .font(.body) .foregroundStyle(.secondary) Capsule().fill(.secondary.opacity(0.5)).frame(width: 36, height: 5) } .padding(.bottom, 6) } .buttonStyle(.plain) .accessibilityLabel(Text("통계 보기")) HStack { Spacer() cornerButton(symbol: "questionmark", label: Text("도움말")) { showHelp = true } } } .padding(.horizontal, 20) .padding(.bottom, 8) } .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 summaryChip(symbol: String, value: String, label: Text, color: Color) -> some View { VStack(spacing: 6) { Image(systemName: symbol) .font(.subheadline.weight(.semibold)) .foregroundStyle(color) Text(value) .font(.title3.bold()) .monospacedDigit() .foregroundStyle(.primary) label .font(.caption) .foregroundStyle(.secondary) } .frame(width: 92) .padding(.vertical, 14) .background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 18)) .shadow(color: .black.opacity(0.06), radius: 10, x: 0, y: 3) } private func cornerButton(symbol: String, label: Text, 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(response: 0.45, dampingFraction: 0.62)) { showStats = true } } private func closeStats() { withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) { 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() } }