import SwiftUI import ProxyCore import GRDB struct BlockListView: View { @State private var isEnabled = IPCManager.shared.isBlockListEnabled @State private var entries: [BlockListEntry] = [] @State private var showAddRule = false @State private var editingEntry: BlockListEntry? @State private var entryToDelete: BlockListEntry? @State private var showDeleteConfirmation = false @State private var observation: AnyDatabaseCancellable? private let rulesRepo = RulesRepository() var body: some View { List { Section { ToggleHeaderView( title: "Block List", description: "Block requests matching these rules. Blocked requests will be dropped or hidden based on the action.", isEnabled: $isEnabled ) .onChange(of: isEnabled) { _, newValue in IPCManager.shared.isBlockListEnabled = newValue IPCManager.shared.post(.configurationChanged) } } .listRowInsets(EdgeInsets()) Section("Rules") { if entries.isEmpty { Text("No block rules") .foregroundStyle(.secondary) .font(.subheadline) } else { ForEach(entries) { entry in Button { editingEntry = entry } label: { VStack(alignment: .leading, spacing: 4) { Text(entry.name ?? entry.urlPattern) .font(.subheadline.weight(.medium)) Text(entry.urlPattern) .font(.caption) .foregroundStyle(.secondary) Text(entry.action.displayName) .font(.caption2) .foregroundStyle(.tertiary) } } .tint(.primary) } .onDelete { indexSet in if let index = indexSet.first { entryToDelete = entries[index] showDeleteConfirmation = true } } } } } .navigationTitle("Block List") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showAddRule = true } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showAddRule) { NewBlockRuleView { entry in var entry = entry try? rulesRepo.insertBlockEntry(&entry) } } .sheet(item: $editingEntry) { entry in NewBlockRuleView(existingEntry: entry) { updated in try? rulesRepo.updateBlockEntry(updated) } } .confirmationDialog("Delete this rule?", isPresented: $showDeleteConfirmation, presenting: entryToDelete) { entry in Button("Delete", role: .destructive) { if let id = entry.id { try? rulesRepo.deleteBlockEntry(id: id) } } } .task { observation = rulesRepo.observeBlockListEntries() .start(in: DatabaseManager.shared.dbPool) { error in print("Block list observation error: \(error)") } onChange: { newEntries in entries = newEntries } } } } // MARK: - New Block Rule struct NewBlockRuleView: View { let existingEntry: BlockListEntry? let onSave: (BlockListEntry) -> Void @State private var name = "" @State private var urlPattern = "" @State private var method = "ANY" @State private var includeSubpaths = true @State private var blockAction: BlockAction = .blockAndHide @Environment(\.dismiss) private var dismiss init(existingEntry: BlockListEntry? = nil, onSave: @escaping (BlockListEntry) -> Void) { self.existingEntry = existingEntry self.onSave = onSave } var body: some View { NavigationStack { Form { Section { TextField("Name (optional)", text: $name) TextField("URL Pattern", text: $urlPattern) .textInputAutocapitalization(.never) .autocorrectionDisabled() } Section { Picker("Method", selection: $method) { Text("ANY").tag("ANY") ForEach(ProxyConstants.httpMethods, id: \.self) { m in Text(m).tag(m) } } Toggle("Include Subpaths", isOn: $includeSubpaths) } Section { Picker("Block Action", selection: $blockAction) { ForEach(BlockAction.allCases, id: \.self) { action in Text(action.displayName).tag(action) } } } } .navigationTitle(existingEntry == nil ? "New Block Rule" : "Edit Block Rule") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { let entry = BlockListEntry( id: existingEntry?.id, name: name.isEmpty ? nil : name, urlPattern: urlPattern, method: method, includeSubpaths: includeSubpaths, blockAction: blockAction, isEnabled: existingEntry?.isEnabled ?? true, createdAt: existingEntry?.createdAt ?? Date().timeIntervalSince1970 ) onSave(entry) dismiss() } .disabled(urlPattern.isEmpty) } } .onAppear { if let entry = existingEntry { name = entry.name ?? "" urlPattern = entry.urlPattern method = entry.method includeSubpaths = entry.includeSubpaths blockAction = entry.action } } } } }