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:
@@ -6,13 +6,16 @@ struct ComposeEditorView: View {
|
||||
|
||||
@State private var method = "GET"
|
||||
@State private var url = ""
|
||||
@State private var headersText = ""
|
||||
@State private var queryText = ""
|
||||
@State private var headers: [(key: String, value: String)] = []
|
||||
@State private var queryParams: [(key: String, value: String)] = []
|
||||
@State private var bodyText = ""
|
||||
@State private var selectedTab: EditorTab = .headers
|
||||
@State private var isSending = false
|
||||
@State private var responseStatus: Int?
|
||||
@State private var responseBody: String?
|
||||
@State private var showHeaderEditor = false
|
||||
@State private var showQueryEditor = false
|
||||
@State private var didLoad = false
|
||||
|
||||
private let composeRepo = ComposeRepository()
|
||||
|
||||
@@ -77,47 +80,103 @@ struct ComposeEditorView: View {
|
||||
}
|
||||
|
||||
// Response
|
||||
if let responseBody {
|
||||
if responseStatus != nil || responseBody != nil {
|
||||
Divider()
|
||||
ScrollView {
|
||||
Text(responseBody)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let status = responseStatus {
|
||||
HStack {
|
||||
Text("Response")
|
||||
.font(.caption.weight(.semibold))
|
||||
StatusBadge(statusCode: status)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
if let body = responseBody {
|
||||
ScrollView {
|
||||
Text(body)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
|
||||
NavigationLink {
|
||||
FullResponseView(statusCode: responseStatus, responseBody: body)
|
||||
} label: {
|
||||
Label("View Full Response", systemImage: "arrow.up.left.and.arrow.down.right")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 200)
|
||||
.background(Color(.systemGray6))
|
||||
}
|
||||
}
|
||||
.navigationTitle("New Request")
|
||||
.navigationTitle("Edit Request")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
Task { await sendRequest() }
|
||||
} label: {
|
||||
Text("Send")
|
||||
.font(.headline)
|
||||
if isSending {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Send")
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(url.isEmpty || isSending)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHeaderEditor) {
|
||||
HeaderEditorSheet(headers: headers) { updated in
|
||||
headers = updated
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showQueryEditor) {
|
||||
QueryEditorSheet(parameters: queryParams) { updated in
|
||||
queryParams = updated
|
||||
}
|
||||
}
|
||||
.task {
|
||||
guard !didLoad else { return }
|
||||
didLoad = true
|
||||
loadFromDB()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tab Views
|
||||
|
||||
private var headerEditor: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if headersText.isEmpty {
|
||||
if headers.isEmpty {
|
||||
EmptyStateView(
|
||||
icon: "list.bullet.rectangle",
|
||||
title: "No Headers",
|
||||
subtitle: "Tap 'Edit Headers' to add headers."
|
||||
)
|
||||
} else {
|
||||
ForEach(headers.indices, id: \.self) { index in
|
||||
HStack {
|
||||
Text(headers[index].key)
|
||||
.font(.subheadline.weight(.medium))
|
||||
Spacer()
|
||||
Text(headers[index].value)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Edit Headers") {
|
||||
// TODO: Open header editor sheet
|
||||
showHeaderEditor = true
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.borderedProminent)
|
||||
@@ -126,16 +185,32 @@ struct ComposeEditorView: View {
|
||||
|
||||
private var queryEditor: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
TextEditor(text: $queryText)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(minHeight: 100)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color(.systemGray4))
|
||||
if queryParams.isEmpty {
|
||||
EmptyStateView(
|
||||
icon: "questionmark.circle",
|
||||
title: "No Query Parameters",
|
||||
subtitle: "Tap 'Edit Parameters' to add query parameters."
|
||||
)
|
||||
Text("Format: key=value, one per line")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(queryParams.indices, id: \.self) { index in
|
||||
HStack {
|
||||
Text(queryParams[index].key)
|
||||
.font(.subheadline.weight(.medium))
|
||||
Spacer()
|
||||
Text(queryParams[index].value)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Edit Parameters") {
|
||||
showQueryEditor = true
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,14 +226,52 @@ struct ComposeEditorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DB Load
|
||||
|
||||
private func loadFromDB() {
|
||||
do {
|
||||
if let request = try composeRepo.fetch(id: requestId) {
|
||||
method = request.method
|
||||
url = request.url ?? ""
|
||||
bodyText = request.body ?? ""
|
||||
headers = decodeKeyValues(request.headers)
|
||||
queryParams = decodeKeyValues(request.queryParameters)
|
||||
responseStatus = request.responseStatus
|
||||
if let data = request.responseBody, let str = String(data: data, encoding: .utf8) {
|
||||
responseBody = str
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to load compose request: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Send
|
||||
|
||||
private func sendRequest() async {
|
||||
guard let requestURL = URL(string: url) else { return }
|
||||
// Build URL with query parameters
|
||||
guard var components = URLComponents(string: url) else { return }
|
||||
|
||||
if !queryParams.isEmpty {
|
||||
var items = components.queryItems ?? []
|
||||
for param in queryParams {
|
||||
items.append(URLQueryItem(name: param.key, value: param.value))
|
||||
}
|
||||
components.queryItems = items
|
||||
}
|
||||
|
||||
guard let requestURL = components.url else { return }
|
||||
|
||||
isSending = true
|
||||
defer { isSending = false }
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = method
|
||||
|
||||
for header in headers {
|
||||
request.setValue(header.value, forHTTPHeaderField: header.key)
|
||||
}
|
||||
|
||||
if !bodyText.isEmpty {
|
||||
request.httpBody = bodyText.data(using: .utf8)
|
||||
}
|
||||
@@ -175,6 +288,46 @@ struct ComposeEditorView: View {
|
||||
}
|
||||
} catch {
|
||||
responseBody = "Error: \(error.localizedDescription)"
|
||||
responseStatus = nil
|
||||
}
|
||||
|
||||
// Save to DB
|
||||
saveToDB()
|
||||
}
|
||||
|
||||
private func saveToDB() {
|
||||
do {
|
||||
if var request = try composeRepo.fetch(id: requestId) {
|
||||
request.method = method
|
||||
request.url = url
|
||||
request.headers = encodeKeyValues(headers)
|
||||
request.queryParameters = encodeKeyValues(queryParams)
|
||||
request.body = bodyText.isEmpty ? nil : bodyText
|
||||
request.responseStatus = responseStatus
|
||||
request.responseBody = responseBody?.data(using: .utf8)
|
||||
request.lastSentAt = Date().timeIntervalSince1970
|
||||
try composeRepo.update(request)
|
||||
}
|
||||
} catch {
|
||||
print("Failed to save compose request: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func encodeKeyValues(_ pairs: [(key: String, value: String)]) -> String? {
|
||||
guard !pairs.isEmpty else { return nil }
|
||||
var dict: [String: String] = [:]
|
||||
for pair in pairs { dict[pair.key] = pair.value }
|
||||
guard let data = try? JSONEncoder().encode(dict) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func decodeKeyValues(_ json: String?) -> [(key: String, value: String)] {
|
||||
guard let json, let data = json.data(using: .utf8),
|
||||
let dict = try? JSONDecoder().decode([String: String].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return dict.map { (key: $0.key, value: $0.value) }.sorted { $0.key < $1.key }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user