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
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FilterChip: Identifiable {
|
||||
struct FilterChip: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let label: String
|
||||
var isSelected: Bool = false
|
||||
@@ -14,21 +14,37 @@ struct FilterChipsView: View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach($chips) { $chip in
|
||||
Button {
|
||||
chip.isSelected.toggle()
|
||||
withAnimation(.spring(response: 0.24, dampingFraction: 0.9)) {
|
||||
chip.isSelected.toggle()
|
||||
}
|
||||
} label: {
|
||||
Text(chip.label)
|
||||
.font(.caption.weight(.medium))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
chip.isSelected ? Color.accentColor : Color(.systemGray5),
|
||||
in: Capsule()
|
||||
)
|
||||
.foregroundStyle(chip.isSelected ? .white : .primary)
|
||||
HStack(spacing: 6) {
|
||||
if chip.isSelected {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.caption2.weight(.bold))
|
||||
}
|
||||
Text(chip.label)
|
||||
.font(.caption.weight(.semibold))
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
.background(
|
||||
chip.isSelected ? Color.accentColor.opacity(0.16) : Color(.systemBackground),
|
||||
in: Capsule()
|
||||
)
|
||||
.overlay(
|
||||
Capsule()
|
||||
.stroke(
|
||||
chip.isSelected ? Color.accentColor.opacity(0.35) : Color.primary.opacity(0.06),
|
||||
lineWidth: 1
|
||||
)
|
||||
)
|
||||
.foregroundStyle(chip.isSelected ? Color.accentColor : .primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
UI/SharedComponents/HexView.swift
Normal file
98
UI/SharedComponents/HexView.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HexView: View {
|
||||
let data: Data
|
||||
|
||||
@State private var showAll = false
|
||||
|
||||
private var displayData: Data {
|
||||
if showAll || data.count <= 1024 {
|
||||
return data
|
||||
}
|
||||
return data.prefix(1024)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header
|
||||
Text("Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII")
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Hex rows
|
||||
let bytes = [UInt8](displayData)
|
||||
let rowCount = (bytes.count + 15) / 16
|
||||
|
||||
ForEach(0..<rowCount, id: \.self) { row in
|
||||
hexRow(bytes: bytes, row: row)
|
||||
}
|
||||
|
||||
// Show more
|
||||
if !showAll && data.count > 1024 {
|
||||
Button {
|
||||
showAll = true
|
||||
} label: {
|
||||
Text("Show All (\(formatBytes(data.count)) total)")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
private func hexRow(bytes: [UInt8], row: Int) -> some View {
|
||||
let offset = row * 16
|
||||
let end = min(offset + 16, bytes.count)
|
||||
let rowBytes = Array(bytes[offset..<end])
|
||||
|
||||
return HStack(spacing: 0) {
|
||||
// Offset
|
||||
Text(String(format: "%08X ", offset))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Hex bytes
|
||||
Text(hexString(rowBytes))
|
||||
|
||||
// ASCII
|
||||
Text(" ")
|
||||
Text(asciiString(rowBytes))
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
}
|
||||
|
||||
private func hexString(_ bytes: [UInt8]) -> String {
|
||||
var result = ""
|
||||
for i in 0..<16 {
|
||||
if i < bytes.count {
|
||||
result += String(format: "%02X ", bytes[i])
|
||||
} else {
|
||||
result += " "
|
||||
}
|
||||
if i == 7 {
|
||||
result += " "
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func asciiString(_ bytes: [UInt8]) -> String {
|
||||
var result = ""
|
||||
for byte in bytes {
|
||||
if byte >= 0x20, byte <= 0x7E {
|
||||
result += String(UnicodeScalar(byte))
|
||||
} else {
|
||||
result += "."
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func formatBytes(_ count: Int) -> String {
|
||||
if count < 1024 { return "\(count) B" }
|
||||
if count < 1_048_576 { return String(format: "%.1f KB", Double(count) / 1024) }
|
||||
return String(format: "%.1f MB", Double(count) / 1_048_576)
|
||||
}
|
||||
}
|
||||
136
UI/SharedComponents/JSONTreeView.swift
Normal file
136
UI/SharedComponents/JSONTreeView.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
import SwiftUI
|
||||
|
||||
struct JSONTreeView: View {
|
||||
let data: Data
|
||||
|
||||
@State private var root: JSONNode?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let root {
|
||||
JSONNodeExpandableView(
|
||||
node: root,
|
||||
collapseByDefault: false,
|
||||
childCollapseThreshold: 50
|
||||
)
|
||||
} else {
|
||||
Text("Invalid JSON")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
root = parseJSON(data)
|
||||
}
|
||||
}
|
||||
|
||||
private func parseJSON(_ data: Data) -> JSONNode? {
|
||||
guard let obj = try? JSONSerialization.jsonObject(with: data) else { return nil }
|
||||
return buildNode(key: nil, value: obj)
|
||||
}
|
||||
|
||||
private func buildNode(key: String?, value: Any) -> JSONNode {
|
||||
if let dict = value as? [String: Any] {
|
||||
let children = dict.sorted(by: { $0.key < $1.key }).map { buildNode(key: $0.key, value: $0.value) }
|
||||
return JSONNode(key: key, kind: .object, displayValue: "{\(dict.count)}", children: children)
|
||||
} else if let arr = value as? [Any] {
|
||||
let children = arr.enumerated().map { buildNode(key: "[\($0.offset)]", value: $0.element) }
|
||||
return JSONNode(key: key, kind: .array, displayValue: "[\(arr.count)]", children: children)
|
||||
} else if let str = value as? String {
|
||||
return JSONNode(key: key, kind: .string, displayValue: "\"\(str)\"", children: [])
|
||||
} else if let num = value as? NSNumber {
|
||||
if CFBooleanGetTypeID() == CFGetTypeID(num) {
|
||||
let boolVal = num.boolValue
|
||||
return JSONNode(key: key, kind: .bool, displayValue: boolVal ? "true" : "false", children: [])
|
||||
}
|
||||
return JSONNode(key: key, kind: .number, displayValue: "\(num)", children: [])
|
||||
} else if value is NSNull {
|
||||
return JSONNode(key: key, kind: .null, displayValue: "null", children: [])
|
||||
} else {
|
||||
return JSONNode(key: key, kind: .string, displayValue: "\(value)", children: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model
|
||||
|
||||
private struct JSONNode: Identifiable {
|
||||
let id = UUID()
|
||||
let key: String?
|
||||
let kind: JSONValueKind
|
||||
let displayValue: String
|
||||
let children: [JSONNode]
|
||||
|
||||
var childCount: Int {
|
||||
children.count + children.reduce(0) { $0 + $1.childCount }
|
||||
}
|
||||
}
|
||||
|
||||
private enum JSONValueKind {
|
||||
case object, array, string, number, bool, null
|
||||
}
|
||||
|
||||
// MARK: - Expandable wrapper (allows per-node expand state)
|
||||
|
||||
private struct JSONNodeExpandableView: View {
|
||||
let node: JSONNode
|
||||
let collapseByDefault: Bool
|
||||
let childCollapseThreshold: Int
|
||||
|
||||
@State private var isExpanded: Bool
|
||||
|
||||
init(node: JSONNode, collapseByDefault: Bool, childCollapseThreshold: Int) {
|
||||
self.node = node
|
||||
self.collapseByDefault = collapseByDefault
|
||||
self.childCollapseThreshold = childCollapseThreshold
|
||||
_isExpanded = State(initialValue: !collapseByDefault)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if node.children.isEmpty {
|
||||
leafRow
|
||||
} else {
|
||||
DisclosureGroup(isExpanded: $isExpanded) {
|
||||
ForEach(node.children) { child in
|
||||
JSONNodeExpandableView(
|
||||
node: child,
|
||||
collapseByDefault: child.childCount > childCollapseThreshold,
|
||||
childCollapseThreshold: childCollapseThreshold
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
labelRow(valueColor: .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var leafRow: some View {
|
||||
labelRow(valueColor: valueColor)
|
||||
}
|
||||
|
||||
private func labelRow(valueColor: Color) -> some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
if let key = node.key {
|
||||
Text(key + ":")
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.bold()
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
Text(node.displayValue)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(valueColor)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private var valueColor: Color {
|
||||
switch node.kind {
|
||||
case .string: return .green
|
||||
case .number: return .blue
|
||||
case .bool: return .orange
|
||||
case .null: return .gray
|
||||
case .object, .array: return .secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
34
UI/SharedComponents/SelectableTextView.swift
Normal file
34
UI/SharedComponents/SelectableTextView.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct SelectableTextView: UIViewRepresentable {
|
||||
let text: String
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = UITextView()
|
||||
textView.isEditable = false
|
||||
textView.isSelectable = true
|
||||
textView.isScrollEnabled = false
|
||||
textView.backgroundColor = .clear
|
||||
textView.textContainerInset = .zero
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.adjustsFontForContentSizeCategory = true
|
||||
textView.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
|
||||
textView.textColor = .label
|
||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
if uiView.text != text {
|
||||
uiView.text = text
|
||||
}
|
||||
}
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
|
||||
let width = proposal.width ?? UIScreen.main.bounds.width
|
||||
let fittingSize = CGSize(width: width, height: .greatestFiniteMagnitude)
|
||||
let size = uiView.sizeThatFits(fittingSize)
|
||||
return CGSize(width: width, height: ceil(size.height))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user