// NotificationsView.swift // Jarvis — notification preferences: breaking alerts + morning/evening briefings // with weather. import SwiftUI struct NotificationsView: View { @EnvironmentObject var manager: NotificationManager @StateObject private var settings = NotificationSettings.shared @Environment(\.dismiss) private var dismiss private var authorized: Bool { manager.authStatus == .authorized || manager.authStatus == .provisional } var body: some View { ZStack { Color.black.ignoresSafeArea() ScrollView { VStack(alignment: .leading, spacing: 24) { header if !authorized { permissionCard } alertsSection briefingSection(title: "MORNING BRIEFING", icon: "sunrise", on: $settings.morningEnabled, hour: $settings.morningHour, minute: $settings.morningMinute) briefingSection(title: "EVENING BRIEFING", icon: "sunset", on: $settings.eveningEnabled, hour: $settings.eveningHour, minute: $settings.eveningMinute) weatherSection testButton } .padding(.horizontal, 20).padding(.vertical, 12) } } .navigationBarBackButtonHidden(false) .task { await manager.refreshAuthStatus() } .onChange(of: settings.morningEnabled) { _, _ in apply() } .onChange(of: settings.eveningEnabled) { _, _ in apply() } .onChange(of: settings.breakingEnabled) { _, _ in apply() } .onChange(of: settings.weatherEnabled) { _, _ in apply() } } private func apply() { Task { await manager.applySettings() } } // MARK: - Header private var header: some View { Text("Notifications").font(.system(size: 22, weight: .heavy)).foregroundStyle(.white) .padding(.top, 4) } private var permissionCard: some View { VStack(alignment: .leading, spacing: 12) { Text("Jarvis needs permission to send briefings and breaking alerts.") .font(.system(size: 14)).foregroundStyle(Color(hex: "BBBBBB")) .fixedSize(horizontal: false, vertical: true) Button { Task { await manager.requestAuthorization() } } label: { Text("Enable notifications") .font(.system(size: 15, weight: .bold)).foregroundStyle(.black) .frame(maxWidth: .infinity).frame(height: 48) .background(Palette.orange).clipShape(RoundedRectangle(cornerRadius: 12)) } } .padding(16).background(Palette.surface).clipShape(RoundedRectangle(cornerRadius: 14)) } // MARK: - Alerts private var alertsSection: some View { section("ALERTS") { toggleRow(icon: "bolt.fill", title: "Breaking & new stories", subtitle: "Notify when a high-signal story breaks", isOn: $settings.breakingEnabled) if settings.breakingEnabled { divider HStack(spacing: 14) { Image(systemName: "dial.medium").font(.system(size: 18)) .foregroundStyle(Color(hex: "999999")).frame(width: 26) Text("Only above signal").font(.system(size: 15, weight: .bold)).foregroundStyle(.white) Spacer() Stepper(value: $settings.breakingMinSignal, in: 0...100, step: 5) {} .labelsHidden().fixedSize() Text("\(settings.breakingMinSignal)") .font(.system(size: 15, weight: .bold, design: .monospaced)) .foregroundStyle(Palette.orange).frame(width: 34, alignment: .trailing) } .padding(.horizontal, 14).padding(.vertical, 12) } } } // MARK: - Briefings private func briefingSection(title: String, icon: String, on: Binding, hour: Binding, minute: Binding) -> some View { section(title) { toggleRow(icon: icon, title: "Daily briefing", subtitle: "Weather + top stories to watch", isOn: on) if on.wrappedValue { divider HStack(spacing: 14) { Image(systemName: "clock").font(.system(size: 18)) .foregroundStyle(Color(hex: "999999")).frame(width: 26) Text("Time").font(.system(size: 15, weight: .bold)).foregroundStyle(.white) Spacer() DatePicker("", selection: timeBinding(hour: hour, minute: minute), displayedComponents: .hourAndMinute) .labelsHidden() .onChange(of: hour.wrappedValue) { _, _ in apply() } .onChange(of: minute.wrappedValue) { _, _ in apply() } } .padding(.horizontal, 14).padding(.vertical, 10) } } } private func timeBinding(hour: Binding, minute: Binding) -> Binding { Binding( get: { Calendar.current.date(from: DateComponents(hour: hour.wrappedValue, minute: minute.wrappedValue)) ?? Date() }, set: { newDate in let c = Calendar.current.dateComponents([.hour, .minute], from: newDate) hour.wrappedValue = c.hour ?? hour.wrappedValue minute.wrappedValue = c.minute ?? minute.wrappedValue } ) } // MARK: - Weather private var weatherSection: some View { section("WEATHER") { toggleRow(icon: "cloud.sun", title: "Include weather", subtitle: "Today's outlook and rain in your briefing", isOn: $settings.weatherEnabled) if settings.weatherEnabled { divider HStack(spacing: 14) { Image(systemName: "mappin.and.ellipse").font(.system(size: 18)) .foregroundStyle(Color(hex: "999999")).frame(width: 26) VStack(alignment: .leading, spacing: 3) { Text("Location").font(.system(size: 12)).foregroundStyle(Color(hex: "888888")) TextField("", text: $settings.locationName, prompt: Text("Kampala").foregroundColor(Color(hex: "444444"))) .font(.system(size: 16, weight: .regular)) .foregroundStyle(.white) .autocorrectionDisabled() .onSubmit { resolveLocation() } if let place = settings.resolvedPlace { Text(place).font(.system(size: 11, design: .monospaced)) .foregroundStyle(Color(hex: "33C25E")) } } Spacer() Button { resolveLocation() } label: { Text("Set").font(.system(size: 13, weight: .bold)).foregroundStyle(.black) .padding(.horizontal, 12).frame(height: 32) .background(Palette.orange).clipShape(Capsule()) } } .padding(.horizontal, 14).padding(.vertical, 12) } } } private func resolveLocation() { Task { settings.latitude = nil; settings.longitude = nil; settings.resolvedPlace = nil await manager.applySettings() // triggers geocode + reschedule } } // MARK: - Test private var testButton: some View { Button { Task { await manager.sendTestBriefing() } } label: { Text("Send a test briefing") .font(.system(size: 15, weight: .bold)).foregroundStyle(.white) .frame(maxWidth: .infinity).frame(height: 50) .background(Palette.surface2).clipShape(RoundedRectangle(cornerRadius: 14)) } .disabled(!authorized) .opacity(authorized ? 1 : 0.5) } // MARK: - Building blocks private func section(_ label: String, @ViewBuilder _ content: () -> C) -> some View { VStack(alignment: .leading, spacing: 10) { Text(label).font(.system(size: 11, weight: .bold, design: .monospaced)) .kerning(0.8).foregroundStyle(Color(hex: "555555")) VStack(spacing: 0) { content() } .background(Palette.surface).clipShape(RoundedRectangle(cornerRadius: 14)) } } private func toggleRow(icon: String, title: String, subtitle: String, isOn: Binding) -> some View { HStack(spacing: 14) { Image(systemName: icon).font(.system(size: 18)).foregroundStyle(Color(hex: "999999")).frame(width: 26) Toggle(isOn: isOn) { VStack(alignment: .leading, spacing: 3) { Text(title).font(.system(size: 16, weight: .heavy)).foregroundStyle(.white) Text(subtitle).font(.system(size: 12)).foregroundStyle(Color(hex: "888888")) .fixedSize(horizontal: false, vertical: true) } } .tint(Palette.orange) } .padding(.horizontal, 14).padding(.vertical, 14) } private var divider: some View { Rectangle().fill(Palette.surface2).frame(height: 1).padding(.leading, 54) } }