// ── Settings View ────────────────────────────────────────────── // Preferences, categories, sounds, about import SwiftUI struct SettingsView: View { @EnvironmentObject var store: TimerStore @EnvironmentObject var notificationManager: CMNotificationManager @ObservedObject private var cloudSync = CloudKitSyncManager.shared @ObservedObject private var crashReporter = CrashReporter.shared @AppStorage("cm_defaultUrgency") private var defaultUrgency = "standard" @AppStorage("cm_defaultCascade") private var defaultCascade = "standard" @AppStorage("cm_hapticEnabled") private var hapticEnabled = true @AppStorage("cm_soundEnabled") private var soundEnabled = true @State private var showFeedback = false var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() List { // Notifications Section { HStack { Label("Notifications", systemImage: "bell.fill") .foregroundStyle(CMColors.text) Spacer() if notificationManager.isAuthorized { Text("Enabled") .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.success) } else { Button("Enable") { Task { await notificationManager.requestPermission() } } .font(CMFonts.body(size: 13, weight: .semibold)) .foregroundStyle(CMColors.accent) } } Toggle(isOn: $hapticEnabled) { Label("Haptic Feedback", systemImage: "iphone.radiowaves.left.and.right") .foregroundStyle(CMColors.text) } .tint(CMColors.accent) Toggle(isOn: $soundEnabled) { Label("Sound", systemImage: "speaker.wave.2.fill") .foregroundStyle(CMColors.text) } .tint(CMColors.accent) } header: { Text("Notifications & Feedback") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) // Defaults Section { Picker(selection: $defaultUrgency) { ForEach(UrgencyLevel.allCases) { level in HStack { Circle() .fill(CMColors.urgencyColor(level)) .frame(width: 8, height: 8) Text(getUrgencyConfig(level).label) } .tag(level.rawValue) } } label: { Label("Default Urgency", systemImage: "exclamationmark.triangle.fill") .foregroundStyle(CMColors.text) } Picker(selection: $defaultCascade) { ForEach(CascadePreset.allCases.filter { $0 != .custom }) { preset in Text(preset.label).tag(preset.rawValue) } } label: { Label("Default Cascade", systemImage: "arrow.down.forward.and.arrow.up.backward") .foregroundStyle(CMColors.text) } } header: { Text("Defaults") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) // Data Section { HStack { Label("Total Timers", systemImage: "number") .foregroundStyle(CMColors.text) Spacer() Text("\(store.timers.count)") .font(CMFonts.mono(size: 14)) .foregroundStyle(CMColors.textSecondary) } Button { store.timers.removeAll { [.completed, .dismissed].contains($0.state) } } label: { Label("Clear History", systemImage: "trash") .foregroundStyle(CMColors.error) } Button { store.timers.removeAll() CMNotificationManager.shared.removeAllNotifications() } label: { Label("Delete All Timers", systemImage: "trash.fill") .foregroundStyle(CMColors.error) } } header: { Text("Data") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) // iCloud Sync Section { Toggle(isOn: $cloudSync.syncEnabled) { Label("iCloud Sync", systemImage: "icloud.fill") .foregroundStyle(CMColors.text) } .tint(CMColors.accent) if cloudSync.syncEnabled { HStack { Label("Last Synced", systemImage: "arrow.triangle.2.circlepath") .foregroundStyle(CMColors.text) Spacer() if let date = cloudSync.lastSyncDate { Text(formatRelativeTime(date, now: Date())) .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) } else { Text("Never") .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) } } } } header: { Text("Sync") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) // Feedback & Diagnostics Section { Button { showFeedback = true } label: { Label("Send Feedback", systemImage: "envelope.fill") .foregroundStyle(CMColors.accent) } HStack { Label("Crash Reports", systemImage: "exclamationmark.triangle") .foregroundStyle(CMColors.text) Spacer() Text("\(crashReporter.diagnosticCount)") .font(CMFonts.mono(size: 13)) .foregroundStyle(CMColors.textMuted) } } header: { Text("Feedback & Diagnostics") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) // About Section { HStack { Label("Version", systemImage: "info.circle") .foregroundStyle(CMColors.text) Spacer() Text("1.0.0 (Phase 3)") .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) } HStack { Label("Product ID", systemImage: "barcode") .foregroundStyle(CMColors.text) Spacer() Text("chronomind") .font(CMFonts.mono(size: 13)) .foregroundStyle(CMColors.textMuted) } Link(destination: URL(string: "https://chronomind.app/privacy")!) { Label("Privacy Policy", systemImage: "hand.raised.fill") .foregroundStyle(CMColors.text) } } header: { Text("About") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) } .scrollContentBackground(.hidden) .listStyle(.insetGrouped) } .navigationTitle("Settings") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(CMColors.surface, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) .sheet(isPresented: $showFeedback) { FeedbackSheet() } } } } // MARK: - Feedback Sheet struct FeedbackSheet: View { @Environment(\.dismiss) private var dismiss @State private var feedbackType: FeedbackType = .bug @State private var feedbackText = "" @State private var includeDeviceInfo = true @State private var submitted = false enum FeedbackType: String, CaseIterable, Identifiable { case bug = "Bug Report" case feature = "Feature Request" case general = "General Feedback" var id: String { rawValue } } var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() if submitted { VStack(spacing: CMSpacing.lg) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 60)) .foregroundStyle(CMColors.success) Text("Thanks for your feedback!") .font(CMFonts.display(size: 20)) .foregroundStyle(CMColors.text) Text("We'll review it and get back to you.") .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textSecondary) } } else { List { Section { Picker("Type", selection: $feedbackType) { ForEach(FeedbackType.allCases) { type in Text(type.rawValue).tag(type) } } .pickerStyle(.segmented) } .listRowBackground(CMColors.surface) Section { TextEditor(text: $feedbackText) .frame(minHeight: 150) .scrollContentBackground(.hidden) .foregroundStyle(CMColors.text) } header: { Text("Describe your feedback") .foregroundStyle(CMColors.textMuted) } .listRowBackground(CMColors.surface) Section { Toggle(isOn: $includeDeviceInfo) { VStack(alignment: .leading, spacing: CMSpacing.xxs) { Text("Include device info") .foregroundStyle(CMColors.text) Text("iOS version, device model, app version") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) } } .tint(CMColors.accent) } .listRowBackground(CMColors.surface) Section { Button { submitFeedback() } label: { HStack { Spacer() Text("Submit Feedback") .font(CMFonts.body(size: 16, weight: .semibold)) .foregroundStyle(.white) Spacer() } .padding(.vertical, CMSpacing.sm) .background(feedbackText.isEmpty ? CMColors.textMuted : CMColors.accent) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) } .disabled(feedbackText.isEmpty) } .listRowBackground(Color.clear) } .scrollContentBackground(.hidden) .listStyle(.insetGrouped) } } .navigationTitle("Send Feedback") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(CMColors.surface, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(submitted ? "Done" : "Cancel") { dismiss() } .foregroundStyle(CMColors.textSecondary) } } } } private func submitFeedback() { // Store feedback locally (send to server in production) let feedback: [String: Any] = [ "type": feedbackType.rawValue, "text": feedbackText, "date": Date().timeIntervalSince1970, "includeDeviceInfo": includeDeviceInfo, ] var stored = UserDefaults.standard.array(forKey: "chronomind-feedback") as? [[String: Any]] ?? [] stored.append(feedback) UserDefaults.standard.set(stored, forKey: "chronomind-feedback") HapticEngine.tap() withAnimation { submitted = true } } }