Files
ProxyIOS/UI/Home/TrafficRowView.swift
Trey t 148bc3887c Add iPad support, auto-pinning, and comprehensive logging
- 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
2026-04-11 12:52:18 -05:00

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())
}
}