Initial project setup - Phases 1-3 complete

This commit is contained in:
Trey t
2026-04-06 11:28:40 -05:00
commit c77e506db5
293 changed files with 14233 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
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)
}
}