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:
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user