- Adaptive iPhone/iPad layout with NavigationSplitView sidebar - Auto-detect SSL-pinned domains, fall back to passthrough - Certificate install via local HTTP server (Safari profile flow) - App Group-backed CA, per-domain leaf cert LRU cache - DB-backed config repository, Darwin notification throttling - Rules engine, breakpoint rules, pinned domain tracking - os.Logger instrumentation across tunnel/proxy/mitm/capture/cert/rules/db/ipc/ui - Fix dyld framework embed, race conditions, thread safety
149 lines
4.9 KiB
Swift
149 lines
4.9 KiB
Swift
import SwiftUI
|
|
import ProxyCore
|
|
|
|
struct TrafficRowView: View {
|
|
let traffic: CapturedTraffic
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
HStack(alignment: .top, spacing: 10) {
|
|
HStack(spacing: 6) {
|
|
MethodBadge(method: traffic.method)
|
|
StatusBadge(statusCode: traffic.statusCode)
|
|
}
|
|
|
|
Spacer(minLength: 12)
|
|
|
|
VStack(alignment: .trailing, spacing: 4) {
|
|
Text(traffic.startDate, format: .dateTime.hour().minute().second().secondFraction(.fractional(3)))
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(traffic.formattedDuration)
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(primaryLine)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(.primary)
|
|
.lineLimit(2)
|
|
|
|
Text(secondaryLine)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
}
|
|
|
|
HStack(spacing: 8) {
|
|
TransferPill(
|
|
systemImage: "arrow.up.circle.fill",
|
|
text: formatBytes(traffic.requestBodySize),
|
|
tint: .green
|
|
)
|
|
TransferPill(
|
|
systemImage: "arrow.down.circle.fill",
|
|
text: responseSizeText,
|
|
tint: .blue
|
|
)
|
|
|
|
if let responseContentType = traffic.responseContentType {
|
|
MetaPill(text: shortContentType(responseContentType))
|
|
} else if let requestContentType = traffic.requestContentType {
|
|
MetaPill(text: shortContentType(requestContentType))
|
|
}
|
|
|
|
if traffic.scheme == "https" && !traffic.isSslDecrypted {
|
|
MetaPill(text: "Encrypted", tint: .orange)
|
|
}
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
if traffic.isPinned {
|
|
Image(systemName: "pin.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(16)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
.fill(Color(.secondarySystemGroupedBackground))
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
|
.strokeBorder(Color.primary.opacity(0.05), lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
private var primaryLine: String {
|
|
let components = URLComponents(string: traffic.url)
|
|
let path = components?.path ?? traffic.url
|
|
let query = components?.percentEncodedQuery.map { "?\($0)" } ?? ""
|
|
let route = path.isEmpty ? traffic.domain : path + query
|
|
return route.isEmpty ? traffic.url : route
|
|
}
|
|
|
|
private var secondaryLine: String {
|
|
if let statusText = traffic.statusText, let statusCode = traffic.statusCode {
|
|
return "\(traffic.domain) • \(statusCode) \(statusText)"
|
|
}
|
|
return traffic.domain
|
|
}
|
|
|
|
private var responseSizeText: String {
|
|
if traffic.responseBodySize > 0 {
|
|
return formatBytes(traffic.responseBodySize)
|
|
}
|
|
if traffic.statusCode == nil {
|
|
return "Pending"
|
|
}
|
|
return "0 B"
|
|
}
|
|
|
|
private func shortContentType(_ contentType: String) -> String {
|
|
let base = contentType.split(separator: ";").first.map(String.init) ?? contentType
|
|
return base.replacingOccurrences(of: "application/", with: "")
|
|
.replacingOccurrences(of: "text/", with: "")
|
|
}
|
|
}
|
|
|
|
private struct TransferPill: View {
|
|
let systemImage: String
|
|
let text: String
|
|
let tint: Color
|
|
|
|
var body: some View {
|
|
Label(text, systemImage: systemImage)
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(tint)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(tint.opacity(0.12), in: Capsule())
|
|
}
|
|
}
|
|
|
|
private struct MetaPill: View {
|
|
let text: String
|
|
var tint: Color = .secondary
|
|
|
|
var body: some View {
|
|
Text(text)
|
|
.font(.caption2.weight(.semibold))
|
|
.foregroundStyle(tint)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(tint.opacity(0.10), in: Capsule())
|
|
}
|
|
}
|