// // SettingsView.swift // SportsTime // import SwiftUI struct SettingsView: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.modelContext) private var modelContext @State private var viewModel = SettingsViewModel() @State private var showResetConfirmation = false @State private var showPaywall = false @State private var showOnboardingPaywall = false #if DEBUG @State private var selectedSyncStatus: EntitySyncStatus? @State private var showSyncLogs = false @State private var exporter = DebugShareExporter() @State private var showExportProgress = false #endif var body: some View { List { // Subscription subscriptionSection // Appearance Mode (Light/Dark/System) appearanceSection // Theme Selection themeSection // Home Screen Animations animationsSection // Sports Preferences sportsSection // Travel Preferences travelSection // Privacy privacySection // Icon Generator iconGeneratorSection // About aboutSection // Reset resetSection #if DEBUG // Debug debugSection // Sync Status syncStatusSection #endif } .scrollContentBackground(.hidden) .themedBackground() .alert("Reset Settings", isPresented: $showResetConfirmation) { Button("Cancel", role: .cancel) { } Button("Reset", role: .destructive) { viewModel.resetToDefaults() } } message: { Text("This will reset all settings to their default values.") } .sheet(isPresented: $showOnboardingPaywall) { OnboardingPaywallView(isPresented: $showOnboardingPaywall) } } // MARK: - Appearance Section private var appearanceSection: some View { Section { ForEach(AppearanceMode.allCases) { mode in Button { withAnimation(.easeInOut(duration: 0.2)) { AppearanceManager.shared.currentMode = mode AnalyticsManager.shared.track(.appearanceChanged(mode: mode.displayName)) } } label: { HStack(spacing: 12) { // Icon ZStack { Circle() .fill(Theme.warmOrange.opacity(0.15)) .frame(width: 32, height: 32) Image(systemName: mode.iconName) .font(.system(size: 16)) .foregroundStyle(Theme.warmOrange) } VStack(alignment: .leading, spacing: 2) { Text(mode.displayName) .font(.body) .foregroundStyle(.primary) Text(mode.description) .font(.caption) .foregroundStyle(.secondary) } Spacer() if AppearanceManager.shared.currentMode == mode { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Theme.warmOrange) .font(.title3) } } .contentShape(Rectangle()) } .buttonStyle(.plain) } } header: { Text("Appearance") } footer: { Text("Choose light, dark, or follow your device settings.") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Theme Section private var themeSection: some View { Section { ForEach(AppTheme.allCases) { theme in Button { withAnimation(.easeInOut(duration: 0.2)) { viewModel.selectedTheme = theme } } label: { HStack(spacing: 12) { // Color preview circles HStack(spacing: -6) { ForEach(Array(theme.previewColors.enumerated()), id: \.offset) { _, color in Circle() .fill(color) .frame(width: 24, height: 24) .overlay( Circle() .stroke(Color.primary.opacity(0.1), lineWidth: 1) ) } } VStack(alignment: .leading, spacing: 2) { Text(theme.displayName) .font(.body) .foregroundStyle(.primary) Text(theme.description) .font(.caption) .foregroundStyle(.secondary) } Spacer() if viewModel.selectedTheme == theme { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Theme.warmOrange) .font(.title3) } } .contentShape(Rectangle()) } .buttonStyle(.plain) } } header: { Text("Theme") } footer: { Text("Choose a color scheme for the app.") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Animations Section private var animationsSection: some View { Section { Toggle(isOn: Binding( get: { DesignStyleManager.shared.animationsEnabled }, set: { DesignStyleManager.shared.animationsEnabled = $0 AnalyticsManager.shared.track(.animationsToggled(enabled: $0)) } )) { Label { VStack(alignment: .leading, spacing: 2) { Text("Animated Background") .font(.body) .foregroundStyle(.primary) Text("Show animated sports graphics on home screen") .font(.caption) .foregroundStyle(.secondary) } } icon: { Image(systemName: "sparkles") .foregroundStyle(Theme.warmOrange) } } } header: { Text("Home Screen") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Sports Section private var sportsSection: some View { Section { ForEach(Sport.supported) { sport in Toggle(isOn: Binding( get: { viewModel.selectedSports.contains(sport) }, set: { _ in viewModel.toggleSport(sport) } )) { Label { Text(sport.displayName) } icon: { Image(systemName: sport.iconName) .foregroundStyle(sportColor(for: sport)) } } } } header: { Text("Favorite Sports") } footer: { Text("Selected sports will be shown by default in schedules and trip planning.") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Travel Section private var travelSection: some View { Section { VStack(alignment: .leading, spacing: 8) { HStack { Text("Max Driving Per Day") Spacer() Text("\(viewModel.maxDrivingHoursPerDay) hours") .foregroundStyle(.secondary) } Slider( value: Binding( get: { Double(viewModel.maxDrivingHoursPerDay) }, set: { viewModel.maxDrivingHoursPerDay = Int($0) } ), in: 2...12, step: 1 ) } } header: { Text("Travel Preferences") } footer: { Text("Trips will be optimized to keep daily driving within this limit.") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Privacy Section private var privacySection: some View { Section { Toggle(isOn: Binding( get: { !AnalyticsManager.shared.isOptedOut }, set: { enabled in if enabled { AnalyticsManager.shared.optIn() } else { AnalyticsManager.shared.optOut() } } )) { Label { VStack(alignment: .leading, spacing: 2) { Text("Share Analytics") .font(.body) .foregroundStyle(.primary) Text("Help improve SportsTime by sharing anonymous usage data") .font(.caption) .foregroundStyle(.secondary) } } icon: { Image(systemName: "chart.bar.xaxis") .foregroundStyle(Theme.warmOrange) } } } header: { Text("Privacy") } footer: { Text("No personal data is collected. Analytics are fully anonymous.") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - About Section private var aboutSection: some View { Section { HStack { Text("Version") Spacer() Text("\(viewModel.appVersion) (\(viewModel.buildNumber))") .foregroundStyle(.secondary) } Link(destination: URL(string: "https://sportstime.88oakapps.com/privacy.html")!) { Label("Privacy Policy", systemImage: "hand.raised") } Link(destination: URL(string: "https://sportstime.88oakapps.com/eula.html")!) { Label("EULA", systemImage: "doc.text") } Link(destination: URL(string: "mailto:support@88oakapps.com")!) { Label("Contact Support", systemImage: "envelope") } } header: { Text("About") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Icon Generator Section private var iconGeneratorSection: some View { Section { NavigationLink { SportsIconImageGeneratorView() .navigationTitle("Icon Generator") .navigationBarTitleDisplayMode(.inline) } label: { Label { VStack(alignment: .leading, spacing: 2) { Text("Sports Icon Generator") .font(.body) .foregroundStyle(.primary) Text("Create shareable icon images") .font(.caption) .foregroundStyle(.secondary) } } icon: { Image(systemName: "photo.badge.plus") .foregroundStyle(Theme.warmOrange) } } } header: { Text("Creative Tools") } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Reset Section private var resetSection: some View { Section { Button(role: .destructive) { showResetConfirmation = true } label: { Label("Reset to Defaults", systemImage: "arrow.counterclockwise") } } .listRowBackground(Theme.cardBackground(colorScheme)) } // MARK: - Debug Section #if DEBUG private var debugSection: some View { Section { Toggle(isOn: Binding( get: { StoreManager.shared.debugProOverride }, set: { StoreManager.shared.debugProOverride = $0 } )) { Label("Override Pro Status", systemImage: "star.fill") } Button { showOnboardingPaywall = true } label: { Label("Show Onboarding Flow", systemImage: "play.circle") } Button { UserDefaults.standard.removeObject(forKey: "hasSeenOnboardingPaywall") } label: { Label("Reset Onboarding Flag", systemImage: "arrow.counterclockwise") } Button { showExportProgress = true Task { await exporter.exportAll(modelContext: modelContext) } } label: { Label("Export All Shareables", systemImage: "square.and.arrow.up.on.square") } Button { showExportProgress = true Task { await exporter.exportAchievementSamples() } } label: { Label("Export Achievement Samples", systemImage: "paintbrush") } Button { showExportProgress = true Task { await exporter.exportProgressSamples() } } label: { Label("Export Progress Samples", systemImage: "chart.bar.fill") } Button { showExportProgress = true Task { await exporter.exportTripSamples() } } label: { Label("Export Trip Samples", systemImage: "car.fill") } Button { Task { await exporter.addAllStadiumVisits(modelContext: modelContext) } } label: { Label("Add All Stadium Visits", systemImage: "mappin.and.ellipse") } } header: { Text("Debug") } footer: { Text("These options are only visible in debug builds.") } .listRowBackground(Theme.cardBackground(colorScheme)) .sheet(isPresented: $showExportProgress) { DebugExportProgressView(exporter: exporter) } } private var syncStatusSection: some View { Section { // Check if sync is disabled let syncState = SyncState.current(in: modelContext) if !syncState.syncEnabled { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "pause.circle.fill") .foregroundStyle(.orange) Text("Sync Paused") .font(.headline) .foregroundStyle(.orange) } if let reason = syncState.syncPausedReason { Text(reason) .font(.caption) .foregroundStyle(.secondary) } if let lastError = syncState.lastSyncError { Text("Last error: \(lastError)") .font(.caption) .foregroundStyle(.red) } Text("Consecutive failures: \(syncState.consecutiveFailures)") .font(.caption) .foregroundStyle(.secondary) } Button { Task { let syncService = CanonicalSyncService() await syncService.resumeSync(context: modelContext) print("[SyncDebug] Sync re-enabled by user") } } label: { Label("Re-enable Sync", systemImage: "arrow.clockwise") } .tint(.blue) } // Overall status header if SyncStatusMonitor.shared.overallSyncInProgress { HStack { ProgressView() .scaleEffect(0.8) Text("Sync in progress...") .font(.subheadline) .foregroundStyle(.secondary) } } else if let lastSync = SyncStatusMonitor.shared.lastFullSyncTime { HStack { Image(systemName: SyncStatusMonitor.shared.allSuccessful ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundStyle(SyncStatusMonitor.shared.allSuccessful ? .green : .orange) Text("Last sync: \(lastSync.formatted(date: .omitted, time: .shortened))") .font(.subheadline) .foregroundStyle(.secondary) if let duration = SyncStatusMonitor.shared.lastFullSyncDuration { Text("(\(String(format: "%.1fs", duration)))") .font(.caption) .foregroundStyle(.tertiary) } } } // Per-entity status rows ForEach(SyncStatusMonitor.shared.orderedStatuses) { status in SyncStatusRow(status: status) { selectedSyncStatus = status } } // Manual sync trigger Button { Task { let syncService = CanonicalSyncService() print("[SyncDebug] Manual sync triggered by user") do { let result = try await syncService.syncAll(context: modelContext) print("[SyncDebug] Manual sync completed: \(result.totalUpdated) records") } catch { print("[SyncDebug] Manual sync failed: \(error)") } } } label: { Label("Trigger Sync Now", systemImage: "arrow.triangle.2.circlepath") } // View sync logs Button { showSyncLogs = true } label: { Label("View Sync Logs", systemImage: "doc.text.magnifyingglass") } } header: { Text("Sync Status") } footer: { Text("Shows CloudKit sync status for each record type. Updated on app foreground.") } .listRowBackground(Theme.cardBackground(colorScheme)) .sheet(item: $selectedSyncStatus) { status in SyncStatusDetailSheet(status: status) } .sheet(isPresented: $showSyncLogs) { SyncLogViewerSheet() } } #endif // MARK: - Subscription Section private var subscriptionSection: some View { Section { if StoreManager.shared.isPro { // Pro user - show manage option HStack { Label { VStack(alignment: .leading, spacing: 4) { Text("SportsTime Pro") .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Active subscription") .font(.caption) .foregroundStyle(.green) } } icon: { Image(systemName: "star.fill") .foregroundStyle(Theme.warmOrange) } Spacer() Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) } Button { if let url = URL(string: "https://apps.apple.com/account/subscriptions") { UIApplication.shared.open(url) } } label: { Label("Manage Subscription", systemImage: "gear") } } else { // Free user - show upgrade option Button { showPaywall = true } label: { HStack { Label { VStack(alignment: .leading, spacing: 4) { Text("Upgrade to Pro") .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Unlimited trips, PDF export, progress tracking") .font(.caption) .foregroundStyle(Theme.textSecondary(colorScheme)) } } icon: { Image(systemName: "star.fill") .foregroundStyle(Theme.warmOrange) } Spacer() Image(systemName: "chevron.right") .foregroundStyle(Theme.textMuted(colorScheme)) } } .buttonStyle(.plain) Button { Task { await StoreManager.shared.restorePurchases(source: "settings") } } label: { Label("Restore Purchases", systemImage: "arrow.clockwise") } } } header: { Text("Subscription") } .listRowBackground(Theme.cardBackground(colorScheme)) .sheet(isPresented: $showPaywall) { PaywallView(source: "settings") } } // MARK: - Helpers private func sportColor(for sport: Sport) -> Color { sport.themeColor } } #Preview { NavigationStack { SettingsView() } } // MARK: - Debug Sync Status Views #if DEBUG /// A row showing sync status for a single entity type struct SyncStatusRow: View { let status: EntitySyncStatus let onInfoTapped: () -> Void var body: some View { HStack(spacing: 12) { // Status indicator Image(systemName: statusIcon) .foregroundStyle(statusColor) .font(.system(size: 14)) .frame(width: 20) // Entity icon and name Image(systemName: status.entityType.iconName) .foregroundStyle(.secondary) .font(.system(size: 14)) .frame(width: 20) Text(status.entityType.rawValue) .font(.body) Spacer() // Record count (if synced) if status.lastSyncTime != nil { Text("\(status.recordCount)") .font(.caption) .foregroundStyle(.secondary) .padding(.horizontal, 8) .padding(.vertical, 2) .background(Color.secondary.opacity(0.1)) .clipShape(Capsule()) } // Info button Button { onInfoTapped() } label: { Image(systemName: "info.circle") .foregroundStyle(.blue) } .buttonStyle(.plain) } } private var statusIcon: String { guard status.lastSyncTime != nil else { return "circle.dotted" } return status.isSuccess ? "checkmark.circle.fill" : "xmark.circle.fill" } private var statusColor: Color { guard status.lastSyncTime != nil else { return .secondary } return status.isSuccess ? .green : .red } } /// Detail sheet showing full sync information for an entity struct SyncStatusDetailSheet: View { let status: EntitySyncStatus @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { List { Section { DetailRow(label: "Entity Type", value: status.entityType.rawValue) DetailRow(label: "Status", value: status.isSuccess ? "Success" : "Failed", valueColor: status.isSuccess ? .green : .red) } Section { if let syncTime = status.lastSyncTime { DetailRow(label: "Last Sync", value: syncTime.formatted(date: .abbreviated, time: .standard)) } else { DetailRow(label: "Last Sync", value: "Never") } DetailRow(label: "Records Synced", value: "\(status.recordCount)") if let duration = status.duration { DetailRow(label: "Duration", value: String(format: "%.2f seconds", duration)) } } if let errorMessage = status.errorMessage { Section("Error") { Text(errorMessage) .font(.caption) .foregroundStyle(.red) } } } .navigationTitle(status.entityType.rawValue) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { dismiss() } } } } .presentationDetents([.medium]) } } /// Helper view for detail rows private struct DetailRow: View { let label: String let value: String var valueColor: Color = .primary var body: some View { HStack { Text(label) .foregroundStyle(.secondary) Spacer() Text(value) .foregroundStyle(valueColor) } } } /// Sheet to view sync logs struct SyncLogViewerSheet: View { @Environment(\.dismiss) private var dismiss @State private var logContent = "" @State private var autoScroll = true var body: some View { NavigationStack { ScrollViewReader { proxy in ScrollView { Text(logContent) .font(.system(.caption, design: .monospaced)) .frame(maxWidth: .infinity, alignment: .leading) .padding() .id("logBottom") } .onChange(of: logContent) { if autoScroll { withAnimation { proxy.scrollTo("logBottom", anchor: .bottom) } } } } .navigationTitle("Sync Logs") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Clear") { SyncLogger.shared.clearLog() logContent = "Log cleared." } } ToolbarItem(placement: .topBarTrailing) { Button("Done") { dismiss() } } ToolbarItem(placement: .bottomBar) { HStack { Button { logContent = SyncLogger.shared.readLog() } label: { Label("Refresh", systemImage: "arrow.clockwise") } Spacer() Toggle("Auto-scroll", isOn: $autoScroll) .toggleStyle(.switch) .labelsHidden() Text("Auto-scroll") .font(.caption) } } } .onAppear { logContent = SyncLogger.shared.readLog() } } } } #endif