mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
270 lines
9.0 KiB
Swift
270 lines
9.0 KiB
Swift
// The MIT License (MIT)
|
||
//
|
||
// Copyright (c) 2020–2022 Alexander Grebenyuk (github.com/kean).
|
||
|
||
import SwiftUI
|
||
import Pulse
|
||
import Combine
|
||
|
||
final class NetworkInspectorSummaryViewModel: ObservableObject {
|
||
@Published var isErrorRawLinkActive = false
|
||
@Published var isRequestRawLinkActive = false
|
||
@Published var isOriginalRequestHeadersLinkActive = false
|
||
@Published var isOriginalQueryItemsLinkActive = false
|
||
@Published var isCurrentRequestHeadersLinkActive = false
|
||
@Published var isCurrentQueryItemsLinkActive = false
|
||
@Published var isResponseRawLinkActive = false
|
||
@Published var isResponseHeadearsRawLinkActive = false
|
||
|
||
private(set) lazy var _progressViewModel = ProgressViewModel(task: task)
|
||
|
||
private let task: NetworkTaskEntity
|
||
private var cancellable: AnyCancellable?
|
||
|
||
init(task: NetworkTaskEntity) {
|
||
self.task = task
|
||
cancellable = task.objectWillChange.sink { [weak self] in self?.refresh() }
|
||
}
|
||
|
||
private func refresh() {
|
||
withAnimation { objectWillChange.send() }
|
||
}
|
||
|
||
var tintColor: Color {
|
||
switch task.state {
|
||
case .pending: return .orange
|
||
case .success: return .green
|
||
case .failure: return .red
|
||
}
|
||
}
|
||
|
||
var statusImageName: String {
|
||
switch task.state {
|
||
case .pending: return "clock.fill"
|
||
case .success: return "checkmark.circle.fill"
|
||
case .failure: return "exclamationmark.octagon.fill"
|
||
}
|
||
}
|
||
|
||
// MARK: - Header
|
||
|
||
var transferViewModel: NetworkInspectorTransferInfoViewModel? {
|
||
guard task.hasMetrics else { return nil }
|
||
return NetworkInspectorTransferInfoViewModel(task: task, taskType: task.type ?? .dataTask)
|
||
}
|
||
|
||
var progressViewModel: ProgressViewModel? {
|
||
guard task.state == .pending else { return nil }
|
||
return _progressViewModel
|
||
}
|
||
|
||
// MARK: - Summary
|
||
|
||
var summaryViewModel: KeyValueSectionViewModel {
|
||
var items: [(String, String?)] = [
|
||
("URL", task.url ?? "–"),
|
||
("Method", task.httpMethod ?? "–")
|
||
]
|
||
|
||
if task.state == .failure || task.state == .success {
|
||
items.append(("Status Code", StatusCodeFormatter.string(for: task.statusCode)))
|
||
if task.duration > 0 {
|
||
items.append(("Duration", DurationFormatter.string(from: task.duration)))
|
||
}
|
||
items.append(("Source", task.isFromCache ? "Cache" : "Network"))
|
||
}
|
||
|
||
var title = task.type?.urlSessionTaskClassName ?? "Summary"
|
||
#if os(watchOS)
|
||
title = title.replacingOccurrences(of: "URLSession", with: "")
|
||
#endif
|
||
return KeyValueSectionViewModel(title: title, color: tintColor, items: items)
|
||
}
|
||
|
||
var errorModel: KeyValueSectionViewModel? {
|
||
KeyValueSectionViewModel.makeErrorDetails(for: task) { [unowned self] in
|
||
isErrorRawLinkActive = true
|
||
}
|
||
}
|
||
|
||
// MARK: - Request (Original)
|
||
|
||
var originalRequestSummary: KeyValueSectionViewModel? {
|
||
task.originalRequest.map(KeyValueSectionViewModel.makeSummary)
|
||
}
|
||
|
||
#if os(iOS) || os(macOS)
|
||
var originalRequestQueryItems: KeyValueSectionViewModel? {
|
||
task.originalRequest?.url.flatMap(URL.init).flatMap {
|
||
KeyValueSectionViewModel.makeQueryItems(for: $0) { [unowned self] in
|
||
self.isOriginalQueryItemsLinkActive = true
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
var originalRequestParameters: KeyValueSectionViewModel? {
|
||
task.originalRequest.map(KeyValueSectionViewModel.makeParameters)
|
||
}
|
||
|
||
var originalRequestHeaders: KeyValueSectionViewModel {
|
||
KeyValueSectionViewModel.makeRequestHeaders(for: task.originalRequest?.headers ?? [:]) { [unowned self] in
|
||
self.isOriginalRequestHeadersLinkActive = true
|
||
}
|
||
}
|
||
|
||
var requestBodySection: KeyValueSectionViewModel {
|
||
guard task.requestBodySize > 0 else {
|
||
return KeyValueSectionViewModel(title: "Request Body", color: .blue)
|
||
}
|
||
let contentType = (task.originalRequest?.headers ?? [:]).first(where: { $0.key == "Content-Type" })?.value ?? "–"
|
||
return KeyValueSectionViewModel(
|
||
title: "Request Body",
|
||
color: .blue,
|
||
action: ActionViewModel(
|
||
action: { [unowned self] in isRequestRawLinkActive = true },
|
||
title: "View"
|
||
),
|
||
items: [
|
||
("Content-Type", contentType),
|
||
("Size", ByteCountFormatter.string(fromByteCount: task.requestBodySize))
|
||
]
|
||
)
|
||
}
|
||
|
||
// MARK: - Request (Current)
|
||
|
||
var currentRequestSummary: KeyValueSectionViewModel? {
|
||
task.currentRequest.map(KeyValueSectionViewModel.makeSummary)
|
||
}
|
||
|
||
#if os(iOS) || os(macOS)
|
||
var currentRequestQueryItems: KeyValueSectionViewModel? {
|
||
task.originalRequest?.url.flatMap(URL.init).flatMap {
|
||
KeyValueSectionViewModel.makeQueryItems(for: $0) { [unowned self] in
|
||
self.isCurrentQueryItemsLinkActive = true
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
var currentRequestParameters: KeyValueSectionViewModel? {
|
||
task.currentRequest.map(KeyValueSectionViewModel.makeParameters)
|
||
}
|
||
|
||
var currentRequestHeaders: KeyValueSectionViewModel {
|
||
KeyValueSectionViewModel.makeRequestHeaders(for: task.currentRequest?.headers ?? [:]) { [unowned self] in
|
||
self.isCurrentRequestHeadersLinkActive = true
|
||
}
|
||
}
|
||
|
||
var currentRequestBodySection: KeyValueSectionViewModel {
|
||
guard task.requestBodySize > 0 else {
|
||
return KeyValueSectionViewModel(title: "Request Body", color: .blue)
|
||
}
|
||
let contentType = task.currentRequest?.headers.first(where: { $0.key == "Content-Type" })?.value ?? "–"
|
||
return KeyValueSectionViewModel(
|
||
title: "Request Body",
|
||
color: .blue,
|
||
action: ActionViewModel(
|
||
action: { [unowned self] in isRequestRawLinkActive = true },
|
||
title: "View"
|
||
),
|
||
items: [
|
||
("Content-Type", contentType),
|
||
("Size", ByteCountFormatter.string(fromByteCount: task.requestBodySize))
|
||
]
|
||
)
|
||
}
|
||
|
||
// MARK: - Response
|
||
|
||
var responseSummary: KeyValueSectionViewModel? {
|
||
task.response.map(KeyValueSectionViewModel.makeSummary)
|
||
}
|
||
|
||
var responseHeaders: KeyValueSectionViewModel {
|
||
KeyValueSectionViewModel.makeResponseHeaders(for: task.response?.headers ?? [:]) { [unowned self] in
|
||
self.isResponseHeadearsRawLinkActive = true
|
||
}
|
||
}
|
||
|
||
var responseBodySection: KeyValueSectionViewModel {
|
||
if task.type == .downloadTask, task.responseBodySize > 0 {
|
||
return KeyValueSectionViewModel(title: "Response Body", color: .indigo, items: [
|
||
("Download Size", ByteCountFormatter.string(fromByteCount: task.responseBodySize))
|
||
])
|
||
}
|
||
guard task.responseBodySize > 0 else {
|
||
return KeyValueSectionViewModel(title: "Response Body", color: .indigo)
|
||
}
|
||
let size = ByteCountFormatter.string(fromByteCount: task.responseBodySize)
|
||
return KeyValueSectionViewModel(
|
||
title: "Response Body",
|
||
color: .indigo,
|
||
action: ActionViewModel(
|
||
action: { [unowned self] in isResponseRawLinkActive = true },
|
||
title: "View"
|
||
),
|
||
items: [
|
||
("Content-Type", task.response?.contentType?.rawValue),
|
||
("Size", task.isFromCache ? size + " (from cache)": size)
|
||
]
|
||
)
|
||
}
|
||
|
||
// MARK: - Timings
|
||
|
||
var timingDetailsViewModel: KeyValueSectionViewModel? {
|
||
guard let taskInterval = task.taskInterval else { return nil }
|
||
return KeyValueSectionViewModel(title: "Timing", color: .orange, items: [
|
||
("Start Date", dateFormatter.string(from: taskInterval.start)),
|
||
("End Date", dateFormatter.string(from: taskInterval.end)),
|
||
("Duration", DurationFormatter.string(from: taskInterval.duration)),
|
||
("Redirect Count", task.redirectCount.description)
|
||
])
|
||
}
|
||
|
||
// MARK: - Destinations
|
||
|
||
var requestBodyViewModel: FileViewerViewModel {
|
||
FileViewerViewModel(
|
||
title: "Request",
|
||
context: task.requestFileViewerContext,
|
||
data: { [weak self] in self?.requestData ?? Data() }
|
||
)
|
||
}
|
||
|
||
private var requestData: Data? {
|
||
task.requestBody?.data
|
||
}
|
||
|
||
var responseBodyViewModel: FileViewerViewModel {
|
||
FileViewerViewModel(
|
||
title: "Response",
|
||
context: task.responseFileViewerContext,
|
||
data: { [weak self] in self?.responseData ?? Data() }
|
||
)
|
||
}
|
||
|
||
private var responseData: Data? {
|
||
task.responseBody?.data
|
||
}
|
||
|
||
#if os(tvOS)
|
||
var timingViewModel: TimingViewModel? {
|
||
guard task.hasMetrics else { return nil }
|
||
return TimingViewModel(task: task)
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// MARK: - Private
|
||
|
||
private let dateFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "en_US")
|
||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
||
return formatter
|
||
}()
|