Skip to content

Commit bd8954e

Browse files
committed
Improve ConsoleTaskCell default design
1 parent 00503d8 commit bd8954e

File tree

4 files changed

+222
-28
lines changed

4 files changed

+222
-28
lines changed

Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift

+10-17
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,18 @@ struct ConsoleMessageCell: View {
3535
HStack {
3636
Text(title)
3737
.lineLimit(1)
38-
#if os(iOS) || os(visionOS)
39-
.font(detailsFont.weight(.medium))
40-
#else
41-
.font(ConsoleConstants.fontTitle.weight(.medium))
42-
#endif
38+
.font(.footnote)
4339
.foregroundColor(titleColor)
4440
Spacer()
4541
Components.makePinView(for: message)
4642
HStack(spacing: 3) {
47-
Text(ConsoleMessageCell.timeFormatter.string(from: message.createdAt))
48-
.lineLimit(1)
49-
.font(detailsFont)
50-
.monospacedDigit()
51-
.foregroundColor(.secondary)
52-
if isDisclosureNeeded {
53-
ListDisclosureIndicator()
54-
}
43+
ConsoleTimestampView(date: message.createdAt)
44+
.overlay(alignment: .trailing) {
45+
if isDisclosureNeeded {
46+
ListDisclosureIndicator()
47+
.offset(x: 11, y: 0)
48+
}
49+
}
5550
}
5651
}
5752
}
@@ -91,11 +86,9 @@ struct ConsoleMessageCell: View {
9186
struct ListDisclosureIndicator: View {
9287
var body: some View {
9388
Image(systemName: "chevron.right")
94-
.foregroundColor(Color.separator)
9589
.lineLimit(1)
96-
.font(ConsoleConstants.fontTitle)
97-
.foregroundColor(.secondary)
98-
.padding(.trailing, -12)
90+
.font(.caption2.weight(.bold))
91+
.foregroundColor(.secondary.opacity(0.33))
9992
}
10093
}
10194

Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift

+202-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,190 @@ import Pulse
77
import Combine
88
import CoreData
99

10+
#if os(iOS)
11+
12+
@available(iOS 15, visionOS 1.0, *)
13+
struct ConsoleTaskCell: View {
14+
@ObservedObject var task: NetworkTaskEntity
15+
var isDisclosureNeeded = false
16+
17+
@ScaledMetric(relativeTo: .body) private var fontMultiplier = 1.0
18+
@ObservedObject private var settings: UserSettings = .shared
19+
@Environment(\.store) private var store: LoggerStore
20+
21+
var body: some View {
22+
VStack(alignment: .leading, spacing: 3) {
23+
header
24+
details
25+
content.padding(.top, 3)
26+
}
27+
}
28+
29+
// MARK: – Header
30+
31+
private var header: some View {
32+
HStack(spacing: 6) {
33+
if task.isMocked {
34+
MockBadgeView()
35+
}
36+
info
37+
Spacer()
38+
ConsoleTimestampView(date: task.createdAt)
39+
}
40+
.overlay(alignment: .leading) {
41+
StatusIndicatorView(state: task.state(in: store))
42+
.offset(x: -14)
43+
}
44+
.overlay(alignment: .trailing) {
45+
if isDisclosureNeeded {
46+
ListDisclosureIndicator()
47+
.offset(x: 11)
48+
}
49+
}
50+
}
51+
52+
private var info: some View {
53+
var text: Text {
54+
let status: Text = Text(ConsoleFormatter.status(for: task, store: store))
55+
.font(.footnote.weight(.medium))
56+
.foregroundColor(task.state == .failure ? .red : .primary)
57+
58+
guard settings.displayOptions.isShowingDetails else {
59+
return status
60+
}
61+
let details = settings.displayOptions.detailsFields
62+
.compactMap(makeInfoText)
63+
.joined(separator: " · ")
64+
guard !details.isEmpty else {
65+
return status
66+
}
67+
return status + Text(" · \(details)").font(.footnote)
68+
}
69+
return text
70+
.tracking(-0.1)
71+
.lineLimit(1)
72+
.foregroundStyle(.secondary)
73+
}
74+
75+
76+
private func makeInfoText(for detail: DisplayOptions.Field) -> String? {
77+
switch detail {
78+
case .method:
79+
task.httpMethod
80+
case .requestSize:
81+
byteCount(for: task.requestBodySize)
82+
case .responseSize:
83+
byteCount(for: task.responseBodySize)
84+
case .responseContentType:
85+
task.responseContentType.map(NetworkLogger.ContentType.init)?.lastComponent.uppercased()
86+
case .duration:
87+
ConsoleFormatter.duration(for: task)
88+
case .host:
89+
task.host
90+
case .statusCode:
91+
task.statusCode != 0 ? task.statusCode.description : nil
92+
case .taskType:
93+
NetworkLogger.TaskType(rawValue: task.taskType)?.urlSessionTaskClassName
94+
case .taskDescription:
95+
task.taskDescription
96+
}
97+
}
98+
99+
// MARK: – Details
100+
101+
@ViewBuilder
102+
private var details: some View {
103+
if let host = task.host, !host.isEmpty {
104+
Text(host)
105+
.lineLimit(1)
106+
.font(.footnote)
107+
.foregroundStyle(.secondary)
108+
}
109+
}
110+
111+
// MARK: – Content
112+
113+
private var content: some View {
114+
var method: Text? {
115+
guard let method = task.httpMethod else {
116+
return nil
117+
}
118+
return Text(method.appending(" "))
119+
.font(contentFont.weight(.medium).smallCaps())
120+
.tracking(-0.3)
121+
}
122+
123+
var main: Text {
124+
Text(task.getFormattedContent(options: settings.displayOptions) ?? "")
125+
.font(contentFont)
126+
}
127+
128+
var text: Text {
129+
if let method {
130+
method + main
131+
} else {
132+
main
133+
}
134+
}
135+
136+
return text
137+
.lineLimit(settings.displayOptions.contentLineLimit)
138+
}
139+
140+
// MARK: - Helpers
141+
142+
private var contentFont: Font {
143+
let baseSize = CGFloat(settings.displayOptions.contentFontSize)
144+
return Font.system(size: baseSize * fontMultiplier)
145+
}
146+
147+
private var detailsFont: Font {
148+
let baseSize = CGFloat(settings.displayOptions.detailsFontSize)
149+
return Font.system(size: baseSize * fontMultiplier).monospacedDigit()
150+
}
151+
152+
private func byteCount(for size: Int64) -> String {
153+
guard size > 0 else { return "0 KB" }
154+
return ByteCountFormatter.string(fromByteCount: size)
155+
}
156+
}
157+
158+
private struct StatusIndicatorView: View {
159+
let state: NetworkTaskEntity.State?
160+
161+
var body: some View {
162+
Image(systemName: "circle.fill")
163+
.foregroundStyle(color)
164+
.font(.system(size: 8))
165+
.clipShape(RoundedRectangle(cornerRadius: 3))
166+
}
167+
168+
private var color: Color {
169+
guard let state else {
170+
return .secondary
171+
}
172+
switch state {
173+
case .pending: return .orange
174+
case .success: return .green
175+
case .failure: return .red
176+
}
177+
}
178+
}
179+
180+
struct ConsoleTimestampView: View {
181+
let date: Date
182+
183+
var body: some View {
184+
Text(ConsoleMessageCell.timeFormatter.string(from: date))
185+
.font(.caption)
186+
.monospacedDigit()
187+
.tracking(-0.5)
188+
.foregroundStyle(.secondary)
189+
}
190+
}
191+
192+
#else
193+
10194
@available(iOS 15, visionOS 1.0, *)
11195
struct ConsoleTaskCell: View {
12196
@ObservedObject var task: NetworkTaskEntity
@@ -135,7 +319,7 @@ struct ConsoleTaskCell: View {
135319
Text(task.httpMethod ?? "GET")
136320
case .requestSize:
137321
makeInfoText("arrow.up", byteCount(for: task.requestBodySize))
138-
case .responseSize:
322+
case .responseSize:
139323
makeInfoText("arrow.down", byteCount(for: task.responseBodySize))
140324
case .responseContentType:
141325
task.responseContentType.map(NetworkLogger.ContentType.init).map {
@@ -211,6 +395,21 @@ private let titleSpacing: CGFloat = 20
211395
private let titleSpacing: CGFloat? = nil
212396
#endif
213397

398+
#endif
399+
400+
#if os(iOS)
401+
@available(iOS 15, visionOS 1.0, *)
402+
struct MockBadgeView: View {
403+
var body: some View {
404+
Text("MOCK")
405+
.foregroundStyle(.background)
406+
.font(.caption2.weight(.semibold))
407+
.padding(EdgeInsets(top: 2, leading: 5, bottom: 1, trailing: 5))
408+
.background(Color.secondary.opacity(0.66))
409+
.clipShape(Capsule())
410+
}
411+
}
412+
#else
214413
@available(iOS 15, visionOS 1.0, *)
215414
struct MockBadgeView: View {
216415
var body: some View {
@@ -237,6 +436,7 @@ struct MockBadgeView: View {
237436
#endif
238437
}
239438
}
439+
#endif
240440

241441
#if DEBUG
242442
@available(iOS 15, visionOS 1.0, *)
@@ -248,3 +448,4 @@ struct ConsoleTaskCell_Previews: PreviewProvider {
248448
}
249449
}
250450
#endif
451+

Sources/PulseUI/Helpers/UserSettings.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public final class UserSettings: ObservableObject {
150150
}
151151

152152
public init(
153-
detailsFields: [Field] = [.method, .requestSize, .responseSize, .duration]
153+
detailsFields: [Field] = [.responseSize, .duration]
154154
) {
155155
self.detailsFields = detailsFields
156156
}

Sources/PulseUI/Mocks/MockTask.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ private let mockReposBody = Bundle.main.url(forResource: "repos", withExtension:
302302

303303
// MARK: - /CreateAPI (GET, redirect)
304304

305-
private let mockCreateAPIOriginalRequest = URLRequest(url: "https://github.com/CreateAPI/Get")
305+
private let mockCreateAPIOriginalRequest = URLRequest(url: "https://github.com/createapi/get")
306306

307307
private let mockCreateAPICurrentRequest = mockCreateAPIOriginalRequest.adding(headers: [
308308
"User-Agent": "Pulse Demo/2.0",
@@ -311,22 +311,22 @@ private let mockCreateAPICurrentRequest = mockCreateAPIOriginalRequest.adding(he
311311
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
312312
])
313313

314-
private let mockCreateAPIRedirectRequest = URLRequest(url: "https://github.com/kean/Get").adding(headers: [
314+
private let mockCreateAPIRedirectRequest = URLRequest(url: "https://github.com/kean/get").adding(headers: [
315315
"User-Agent": "Pulse Demo/2.0",
316316
"Accept-Encoding": "gzip",
317317
"Accept-Language": "en-us",
318318
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
319319
])
320320

321-
private let mockCreateaAPIRedirectResponse = HTTPURLResponse(url: "https://github.com/CreateAPI/Get", statusCode: 301, headers: [
321+
private let mockCreateaAPIRedirectResponse = HTTPURLResponse(url: "https://github.com/createapi/get", statusCode: 301, headers: [
322322
"Content-Type": "text/html; charset=utf-8",
323323
"Location": "https://github.com/kean/Get",
324324
"Cache-Control": "no-cache",
325325
"Content-Length": "0",
326326
"Server": "GitHub.com"
327327
])
328328

329-
private let mockCreateaAPIResponse = HTTPURLResponse(url: "https://github.com/kean/Get", statusCode: 200, headers: [
329+
private let mockCreateaAPIResponse = HTTPURLResponse(url: "https://github.com/kean/get", statusCode: 200, headers: [
330330
"Content-Type": "text/html; charset=utf-8",
331331
"Content-Length": "90",
332332
"Cache-Control": "no-store"
@@ -349,7 +349,7 @@ private let mockCreateaAPIBody = """
349349
// MARK: - PATCH
350350

351351
private let mockPatchRepoOriginalRequest: URLRequest = {
352-
var request = URLRequest(url: "https://github.com/repos/kean/Nuke", method: "PATCH")
352+
var request = URLRequest(url: "https://github.com/repos/kean/nuke", method: "PATCH")
353353
request.httpBody = """
354354
name=ImageKit&description=Image%20Loading%Framework&private=false
355355
""".data(using: .utf8)
@@ -364,7 +364,7 @@ private let mockPatchRepoCurrentRequest = mockPatchRepoOriginalRequest.adding(he
364364
"Accept": "application/vnd.github+json"
365365
])
366366

367-
private let mockPatchRepoResponse = HTTPURLResponse(url: "https://github.com/repos/kean/Nuke", statusCode: 200, headers: [
367+
private let mockPatchRepoResponse = HTTPURLResponse(url: "https://github.com/repos/kean/nuke", statusCode: 200, headers: [
368368
"Content-Length": "165061",
369369
"Content-Type": "application/json; charset=utf-8",
370370
"Cache-Control": "no-store",
@@ -447,7 +447,7 @@ private let mockPatchRepoDecodingError: Error = {
447447

448448
// MARK: - Download (GET)
449449

450-
private let mockDownloadNukeOriginalRequest = URLRequest(url: "https://github.com/kean/Nuke/archive/tags/11.0.0.zip")
450+
private let mockDownloadNukeOriginalRequest = URLRequest(url: "https://github.com/kean/nuke/archive/tags/11.0.0.zip")
451451

452452
private let mockDownloadNukeCurrentRequest = mockDownloadNukeOriginalRequest.adding(headers: [
453453
"User-Agent": "Pulse Demo/2.0",
@@ -456,7 +456,7 @@ private let mockDownloadNukeCurrentRequest = mockDownloadNukeOriginalRequest.add
456456
"Accept": "*/*"
457457
])
458458

459-
private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0", statusCode: 302, headers: [
459+
private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/nuke/zip/tags/11.0.0", statusCode: 302, headers: [
460460
"Server": "GitHub.com",
461461
"Content-Type": "text/html; charset=utf-8",
462462
"Location": "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0",
@@ -465,7 +465,7 @@ private let mockDownloadNukeRedirectResponse = HTTPURLResponse(url: "https://cod
465465
"Content-Security-Policy": "default-src 'none'; base-uri 'self'; block-all-mixed-content; child-src github.com/assets-cdn/worker/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com objects-origin.githubusercontent.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com cdn.optimizely.com logx.optimizely.com/v1/events *.actions.githubusercontent.com wss://*.actions.githubusercontent.com online.visualstudio.com/api/v1/locations github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com"
466466
])
467467

468-
private let mockDownloadNukeResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/Nuke/zip/tags/11.0.0", statusCode: 200, headers: [
468+
private let mockDownloadNukeResponse = HTTPURLResponse(url: "https://codeload.github.com/kean/nuke/zip/tags/11.0.0", statusCode: 200, headers: [
469469
"Content-Type": "application/zip",
470470
"Content-Disposition": "attachment; filename=Nuke-11.0.0.zip",
471471
"Etag": "W/\\\"4358c3c3d9bd5a22f6d86b47cbe567417fa1efc8df6beaa54c1730caf6ad86da\\\"",

0 commit comments

Comments
 (0)