import SwiftUI import ProxyCore import GRDB struct SSLProxyingListView: View { @State private var isEnabled = IPCManager.shared.isSSLProxyingEnabled @State private var entries: [SSLProxyingEntry] = [] @State private var showAddInclude = false @State private var showAddExclude = false @State private var editingEntry: SSLProxyingEntry? @State private var entryToDelete: SSLProxyingEntry? @State private var showDeleteConfirmation = false @State private var observation: AnyDatabaseCancellable? private let rulesRepo = RulesRepository() var includeEntries: [SSLProxyingEntry] { entries.filter(\.isInclude) } var excludeEntries: [SSLProxyingEntry] { entries.filter { !$0.isInclude } } var body: some View { List { Section { ToggleHeaderView( title: "SSL Proxying", description: "Decrypt HTTPS traffic from included domains. Excluded domains are always passed through.", isEnabled: $isEnabled ) .onChange(of: isEnabled) { _, newValue in IPCManager.shared.isSSLProxyingEnabled = newValue IPCManager.shared.post(.configurationChanged) } } .listRowInsets(EdgeInsets()) Section("Include") { if includeEntries.isEmpty { Text("No include entries") .foregroundStyle(.secondary) .font(.subheadline) } else { ForEach(includeEntries) { entry in Button { editingEntry = entry } label: { Text(entry.domainPattern) .foregroundStyle(.primary) } } .onDelete { indexSet in if let index = indexSet.first { entryToDelete = includeEntries[index] showDeleteConfirmation = true } } } } Section("Exclude") { if excludeEntries.isEmpty { Text("No exclude entries") .foregroundStyle(.secondary) .font(.subheadline) } else { ForEach(excludeEntries) { entry in Button { editingEntry = entry } label: { Text(entry.domainPattern) .foregroundStyle(.primary) } } .onDelete { indexSet in if let index = indexSet.first { entryToDelete = excludeEntries[index] showDeleteConfirmation = true } } } } } .navigationTitle("SSL Proxying") .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { Button("Add Include Entry") { showAddInclude = true } Button("Add Exclude Entry") { showAddExclude = true } Divider() Button("Clear All Rules", role: .destructive) { try? rulesRepo.deleteAllSSLEntries() IPCManager.shared.post(.configurationChanged) } } label: { Image(systemName: "ellipsis.circle") } } } .sheet(isPresented: $showAddInclude) { DomainEntrySheet(title: "New Include Entry", isInclude: true) { pattern in var entry = SSLProxyingEntry(domainPattern: pattern, isInclude: true) try? rulesRepo.insertSSLEntry(&entry) IPCManager.shared.isSSLProxyingEnabled = true IPCManager.shared.post(.configurationChanged) } } .sheet(isPresented: $showAddExclude) { DomainEntrySheet(title: "New Exclude Entry", isInclude: false) { pattern in var entry = SSLProxyingEntry(domainPattern: pattern, isInclude: false) try? rulesRepo.insertSSLEntry(&entry) IPCManager.shared.post(.configurationChanged) } } .sheet(item: $editingEntry) { entry in DomainEntrySheet( title: entry.isInclude ? "Edit Include Entry" : "Edit Exclude Entry", isInclude: entry.isInclude, existingEntry: entry ) { pattern in var updated = entry updated.domainPattern = pattern try? rulesRepo.updateSSLEntry(updated) if updated.isInclude { IPCManager.shared.isSSLProxyingEnabled = true } IPCManager.shared.post(.configurationChanged) } } .confirmationDialog("Delete this rule?", isPresented: $showDeleteConfirmation, presenting: entryToDelete) { entry in Button("Delete", role: .destructive) { if let id = entry.id { try? rulesRepo.deleteSSLEntry(id: id) } } } .task { observation = rulesRepo.observeSSLEntries() .start(in: DatabaseManager.shared.dbPool) { error in print("SSL observation error: \(error)") } onChange: { newEntries in entries = newEntries } } } } // MARK: - Domain Entry Sheet struct DomainEntrySheet: View { let title: String let isInclude: Bool let existingEntry: SSLProxyingEntry? let onSave: (String) -> Void @State private var domainPattern = "" @Environment(\.dismiss) private var dismiss init(title: String, isInclude: Bool, existingEntry: SSLProxyingEntry? = nil, onSave: @escaping (String) -> Void) { self.title = title self.isInclude = isInclude self.existingEntry = existingEntry self.onSave = onSave } var body: some View { NavigationStack { Form { Section { TextField("Domain Pattern", text: $domainPattern) .textInputAutocapitalization(.never) .autocorrectionDisabled() } footer: { Text("Supports wildcards: * (zero or more) and ? (single character). Example: *.example.com") } } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { onSave(domainPattern) dismiss() } .disabled(domainPattern.isEmpty) } } .onAppear { if let entry = existingEntry { domainPattern = entry.domainPattern } } } } }