import SwiftUI import ProxyCore import GRDB struct BreakpointRulesView: View { @State private var isEnabled = IPCManager.shared.isBreakpointEnabled @State private var rules: [BreakpointRule] = [] @State private var showAddRule = false @State private var editingRule: BreakpointRule? @State private var ruleToDelete: BreakpointRule? @State private var showDeleteConfirmation = false @State private var observation: AnyDatabaseCancellable? @State private var selectedTab = 0 private let rulesRepo = RulesRepository() var body: some View { List { Section { ToggleHeaderView( title: "Breakpoint", description: "Pause and modify HTTP requests and responses in real-time before they reach the server or the app.", isEnabled: $isEnabled ) .onChange(of: isEnabled) { _, newValue in IPCManager.shared.isBreakpointEnabled = newValue IPCManager.shared.post(.configurationChanged) } } .listRowInsets(EdgeInsets()) Section { Picker("Tab", selection: $selectedTab) { Text("Rules").tag(0) Text("Waiting").tag(1) } .pickerStyle(.segmented) .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .listRowBackground(Color.clear) } if selectedTab == 0 { Section("Rules") { if rules.isEmpty { EmptyStateView( icon: "pause.circle", title: "No Breakpoint Rules", subtitle: "Tap + to create a new breakpoint rule." ) } else { ForEach(rules) { rule in Button { editingRule = rule } label: { VStack(alignment: .leading, spacing: 4) { Text(rule.name ?? rule.urlPattern) .font(.subheadline.weight(.medium)) Text(rule.urlPattern) .font(.caption) .foregroundStyle(.secondary) HStack(spacing: 8) { if rule.interceptRequest { Text("Request") .font(.caption2) .foregroundStyle(.blue) } if rule.interceptResponse { Text("Response") .font(.caption2) .foregroundStyle(.green) } } } } .tint(.primary) } .onDelete { indexSet in if let index = indexSet.first { ruleToDelete = rules[index] showDeleteConfirmation = true } } } } } else { Section("Waiting") { EmptyStateView( icon: "clock", title: "No Waiting Breakpoints", subtitle: "Breakpoints will appear here when a request is paused." ) } } } .navigationTitle("Breakpoint") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showAddRule = true } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showAddRule) { AddBreakpointRuleSheet(rulesRepo: rulesRepo, isPresented: $showAddRule) } .sheet(item: $editingRule) { rule in AddBreakpointRuleSheet(rulesRepo: rulesRepo, existingRule: rule, isPresented: .constant(true)) } .confirmationDialog("Delete this rule?", isPresented: $showDeleteConfirmation, presenting: ruleToDelete) { rule in Button("Delete", role: .destructive) { if let id = rule.id { try? rulesRepo.deleteBreakpointRule(id: id) } } } .task { observation = rulesRepo.observeBreakpointRules() .start(in: DatabaseManager.shared.dbPool) { error in print("Breakpoint observation error: \(error)") } onChange: { newRules in rules = newRules } } } } // MARK: - Add Breakpoint Rule Sheet private struct AddBreakpointRuleSheet: View { let rulesRepo: RulesRepository let existingRule: BreakpointRule? @Binding var isPresented: Bool @Environment(\.dismiss) private var dismiss @State private var name = "" @State private var urlPattern = "" @State private var method = "ANY" @State private var interceptRequest = true @State private var interceptResponse = true private var methods: [String] { ["ANY"] + ProxyConstants.httpMethods } init(rulesRepo: RulesRepository, existingRule: BreakpointRule? = nil, isPresented: Binding) { self.rulesRepo = rulesRepo self.existingRule = existingRule self._isPresented = isPresented } var body: some View { NavigationStack { Form { Section { TextField("Name (optional)", text: $name) TextField("URL Pattern (e.g. */api/*)", text: $urlPattern) .textInputAutocapitalization(.never) .autocorrectionDisabled() } Section { Picker("Method", selection: $method) { ForEach(methods, id: \.self) { m in Text(m).tag(m) } } } Section { Toggle("Intercept Request", isOn: $interceptRequest) Toggle("Intercept Response", isOn: $interceptResponse) } Section { Button("Save") { if let existing = existingRule { let updated = BreakpointRule( id: existing.id, name: name.isEmpty ? nil : name, urlPattern: urlPattern, method: method, interceptRequest: interceptRequest, interceptResponse: interceptResponse, isEnabled: existing.isEnabled, createdAt: existing.createdAt ) try? rulesRepo.updateBreakpointRule(updated) } else { var rule = BreakpointRule( name: name.isEmpty ? nil : name, urlPattern: urlPattern, method: method, interceptRequest: interceptRequest, interceptResponse: interceptResponse ) try? rulesRepo.insertBreakpointRule(&rule) } isPresented = false dismiss() } .disabled(urlPattern.isEmpty) } } .navigationTitle(existingRule == nil ? "New Breakpoint Rule" : "Edit Breakpoint Rule") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { isPresented = false dismiss() } } } .onAppear { if let rule = existingRule { name = rule.name ?? "" urlPattern = rule.urlPattern method = rule.method interceptRequest = rule.interceptRequest interceptResponse = rule.interceptResponse } } } } }