import SwiftUI import ProxyCore struct RequestDetailView: View { let trafficId: Int64 @State private var traffic: CapturedTraffic? @State private var selectedSegment: Segment = .request private let trafficRepo = TrafficRepository() enum Segment: String, CaseIterable { case request = "Request" case response = "Response" } var body: some View { Group { if let traffic { VStack(spacing: 0) { Picker("Segment", selection: $selectedSegment) { ForEach(Segment.allCases, id: \.self) { segment in Text(segment.rawValue).tag(segment) } } .pickerStyle(.segmented) .padding() ScrollView { VStack(alignment: .leading, spacing: 16) { switch selectedSegment { case .request: requestContent(traffic) case .response: responseContent(traffic) } } .padding() } } } else { ProgressView() } } .navigationTitle(traffic?.domain ?? "Request") .navigationBarTitleDisplayMode(.inline) .toolbar { if let traffic { ToolbarItem(placement: .topBarTrailing) { Button { try? trafficRepo.togglePin(id: trafficId, isPinned: !traffic.isPinned) self.traffic?.isPinned.toggle() } label: { Image(systemName: traffic.isPinned ? "pin.fill" : "pin") } } } } .task { traffic = try? trafficRepo.traffic(byId: trafficId) } } // MARK: - Request Content @ViewBuilder private func requestContent(_ traffic: CapturedTraffic) -> some View { // General DisclosureGroup("General") { VStack(alignment: .leading, spacing: 12) { KeyValueRow(key: "URL", value: traffic.url) KeyValueRow(key: "Method", value: traffic.method) KeyValueRow(key: "Scheme", value: traffic.scheme) KeyValueRow(key: "Time", value: traffic.startDate.formatted(.dateTime)) if let duration = traffic.durationMs { KeyValueRow(key: "Duration", value: "\(duration) ms") } if let status = traffic.statusCode { KeyValueRow(key: "Status", value: "\(status) \(traffic.statusText ?? "")") } } .padding(.vertical, 8) } // Headers let requestHeaders = traffic.decodedRequestHeaders if !requestHeaders.isEmpty { DisclosureGroup("Headers (\(requestHeaders.count))") { VStack(alignment: .leading, spacing: 12) { ForEach(requestHeaders.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in KeyValueRow(key: key, value: value) } } .padding(.vertical, 8) } } // Query Parameters let queryParams = traffic.decodedQueryParameters if !queryParams.isEmpty { DisclosureGroup("Query (\(queryParams.count))") { VStack(alignment: .leading, spacing: 12) { ForEach(queryParams.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in KeyValueRow(key: key, value: value) } } .padding(.vertical, 8) } } // Body if let body = traffic.requestBody, !body.isEmpty { DisclosureGroup("Body (\(formatBytes(body.count)))") { bodyView(data: body, contentType: traffic.requestContentType) .padding(.vertical, 8) } } } // MARK: - Response Content @ViewBuilder private func responseContent(_ traffic: CapturedTraffic) -> some View { if let status = traffic.statusCode { // Status HStack { StatusBadge(statusCode: status) Text(traffic.statusText ?? "") .font(.subheadline) } .padding(.vertical, 4) } // Headers let responseHeaders = traffic.decodedResponseHeaders if !responseHeaders.isEmpty { DisclosureGroup("Headers (\(responseHeaders.count))") { VStack(alignment: .leading, spacing: 12) { ForEach(responseHeaders.sorted(by: { $0.key < $1.key }), id: \.key) { key, value in KeyValueRow(key: key, value: value) } } .padding(.vertical, 8) } } // Body if let body = traffic.responseBody, !body.isEmpty { DisclosureGroup("Body (\(formatBytes(body.count)))") { bodyView(data: body, contentType: traffic.responseContentType) .padding(.vertical, 8) } } if traffic.statusCode == nil { EmptyStateView( icon: "clock", title: "Waiting for Response", subtitle: "The response has not been received yet." ) } } // MARK: - Body View @ViewBuilder private func bodyView(data: Data, contentType: String?) -> some View { if let contentType, contentType.contains("json"), let json = try? JSONSerialization.jsonObject(with: data), let pretty = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted), let string = String(data: pretty, encoding: .utf8) { ScrollView(.horizontal) { Text(string) .font(.system(.caption, design: .monospaced)) .textSelection(.enabled) } } else if let string = String(data: data, encoding: .utf8) { Text(string) .font(.system(.caption, design: .monospaced)) .textSelection(.enabled) } else { Text("\(data.count) bytes (binary)") .font(.caption) .foregroundStyle(.secondary) } } private func formatBytes(_ bytes: Int) -> String { if bytes < 1024 { return "\(bytes) B" } if bytes < 1_048_576 { return String(format: "%.1f KB", Double(bytes) / 1024) } return String(format: "%.1f MB", Double(bytes) / 1_048_576) } }