import SwiftUI struct SettingsView: View { @EnvironmentObject var store: ConnectionStore @EnvironmentObject var lanMonitor: LANMonitor var body: some View { ZStack { Color.white.ignoresSafeArea() ScrollView { VStack(spacing: AppTheme.sectionGap) { LANTriggerSection() mediaSection quietHoursSection connectivitySection remoteAccessSection notificationSection } .padding(.horizontal, AppTheme.hPad) .padding(.top, 20) .padding(.bottom, 48) } } .navigationTitle("Settings") .navigationBarTitleDisplayMode(.inline) } // MARK: — Media private var mediaSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel("WHAT TO BACK UP") VStack(spacing: 0) { ToggleRow(icon: "photo", title: "Photos", isOn: $store.backupFilter.includePhotos) hairline ToggleRow(icon: "video", title: "Videos", isOn: $store.backupFilter.includeVideos) hairline ToggleRow(icon: "camera.viewfinder", title: "Screenshots", isOn: $store.backupFilter.includeScreenshots) hairline ToggleRow(icon: "rays", title: "RAW", isOn: $store.backupFilter.includeRAW) hairline ToggleRow(icon: "doc.badge.clock", title: "New files only", subtitle: "Skip files already on NAS", isOn: $store.backupFilter.newFilesOnly) } .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) } } // MARK: — Quiet hours private var quietHoursSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel("QUIET HOURS") VStack(spacing: 0) { ToggleRow(icon: "moon", title: "Quiet hours", subtitle: "No auto-backup during this window", isOn: $store.quietHoursEnabled) if store.quietHoursEnabled { hairline timePickerRow(label: "Start", hour: $store.quietHoursStart) hairline timePickerRow(label: "End", hour: $store.quietHoursEnd) } hairline ToggleRow(icon: "bolt", title: "Only while charging", subtitle: "Preserve battery on large libraries", isOn: $store.chargingOnlyMode) } .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: store.quietHoursEnabled) } } private func timePickerRow(label: String, hour: Binding) -> some View { HStack { Text(label) .font(AppTheme.body()) .foregroundStyle(AppTheme.ink) Spacer() Picker(label, selection: hour) { ForEach(0..<24, id: \.self) { h in Text(String(format: "%02d:00", h)).tag(h) } } .pickerStyle(.menu) .tint(AppTheme.inkSecondary) } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 12) } // MARK: — Connectivity private var connectivitySection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel("CONNECTIVITY") VStack(spacing: 0) { ToggleRow( icon: "antenna.radiowaves.left.and.right", title: "Allow over cellular", subtitle: "Uses mobile data when Wi-Fi is unavailable", isOn: $store.allowCellularBackup ) // Live status chip if lanMonitor.isOnCellular { hairline HStack(spacing: 8) { Circle() .fill(store.allowCellularBackup ? AppTheme.positive : AppTheme.destructive) .frame(width: 6, height: 6) Text(store.allowCellularBackup ? "On cellular — backup allowed" : "On cellular — backup blocked") .font(AppTheme.micro(11)) .foregroundStyle(store.allowCellularBackup ? AppTheme.positive : AppTheme.destructive) } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 10) } } .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: lanMonitor.isOnCellular) } } // MARK: — Remote Access (Tailscale) private var remoteAccessSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel("REMOTE ACCESS") VStack(spacing: 0) { ToggleRow( icon: "network.badge.shield.half.filled", title: "Use Tailscale when remote", subtitle: "Connects via VPN when not on home network", isOn: $store.useTailscaleWhenRemote ) if store.useTailscaleWhenRemote { hairline // Tailscale host input HStack(spacing: 12) { Image(systemName: "server.rack") .font(.system(size: 14, weight: .regular)) .foregroundStyle(AppTheme.inkSecondary) .frame(width: 20) VStack(alignment: .leading, spacing: 3) { Text("Tailscale NAS host") .font(AppTheme.micro()) .foregroundStyle(AppTheme.inkTertiary) TextField("100.x.x.x or nas.tail-xxxxx.ts.net", text: $store.tailscaleHost) .font(AppTheme.body()) .foregroundStyle(AppTheme.ink) .keyboardType(.URL) .autocorrectionDisabled() .textInputAutocapitalization(.never) } } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 12) hairline // Tailscale status + open button HStack(spacing: 10) { Circle() .fill(lanMonitor.isTailscaleActive ? AppTheme.positive : AppTheme.inkQuaternary) .frame(width: 7, height: 7) Text(lanMonitor.isTailscaleActive ? "Tailscale connected" : "Tailscale not active") .font(AppTheme.caption()) .foregroundStyle(lanMonitor.isTailscaleActive ? AppTheme.positive : AppTheme.inkTertiary) Spacer() if !lanMonitor.isTailscaleActive { Button { lanMonitor.openTailscaleApp() } label: { Text("Open Tailscale") .font(AppTheme.micro()) .foregroundStyle(AppTheme.ink) .padding(.horizontal, 10) .padding(.vertical, 5) .background(AppTheme.surfaceSunken) .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) } .buttonStyle(ScaleButtonStyle()) } } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 12) } } .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: store.useTailscaleWhenRemote) .animation(.spring(response: 0.25, dampingFraction: 0.8), value: lanMonitor.isTailscaleActive) } } // MARK: — Notifications private var notificationSection: some View { VStack(alignment: .leading, spacing: 8) { sectionLabel("NOTIFICATIONS") VStack(spacing: 0) { notifRow(icon: "bell", title: "On start", value: "Off") hairline notifRow(icon: "checkmark.circle", title: "On complete", value: "On") hairline notifRow(icon: "exclamationmark.triangle", title: "On errors", value: "On") } .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) } } private func notifRow(icon: String, title: String, value: String) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 14, weight: .regular)) .foregroundStyle(AppTheme.inkSecondary) .frame(width: 20) Text(title) .font(AppTheme.body()) .foregroundStyle(AppTheme.ink) Spacer() Text(value) .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 14) } private var hairline: some View { Rectangle() .fill(AppTheme.separator) .frame(height: 0.5) .padding(.leading, AppTheme.cardPad) } }