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 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 Text(entry.domainPattern) } .onDelete { indexSet in for index in indexSet { if let id = includeEntries[index].id { try? rulesRepo.deleteSSLEntry(id: id) } } } } } Section("Exclude") { if excludeEntries.isEmpty { Text("No exclude entries") .foregroundStyle(.secondary) .font(.subheadline) } else { ForEach(excludeEntries) { entry in Text(entry.domainPattern) } .onDelete { indexSet in for index in indexSet { if let id = excludeEntries[index].id { try? rulesRepo.deleteSSLEntry(id: id) } } } } } } .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() } } 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) } } .sheet(isPresented: $showAddExclude) { DomainEntrySheet(title: "New Exclude Entry", isInclude: false) { pattern in var entry = SSLProxyingEntry(domainPattern: pattern, isInclude: false) try? rulesRepo.insertSSLEntry(&entry) } } .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 onSave: (String) -> Void @State private var domainPattern = "" @Environment(\.dismiss) private var dismiss 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) } } } } }