@@ -7,6 +7,190 @@ import Pulse
7
7
import Combine
8
8
import CoreData
9
9
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
+
10
194
@available ( iOS 15 , visionOS 1 . 0 , * )
11
195
struct ConsoleTaskCell : View {
12
196
@ObservedObject var task : NetworkTaskEntity
@@ -135,7 +319,7 @@ struct ConsoleTaskCell: View {
135
319
Text ( task. httpMethod ?? " GET " )
136
320
case . requestSize:
137
321
makeInfoText ( " arrow.up " , byteCount ( for: task. requestBodySize) )
138
- case . responseSize:
322
+ case . responseSize:
139
323
makeInfoText ( " arrow.down " , byteCount ( for: task. responseBodySize) )
140
324
case . responseContentType:
141
325
task. responseContentType. map ( NetworkLogger . ContentType. init) . map {
@@ -211,6 +395,21 @@ private let titleSpacing: CGFloat = 20
211
395
private let titleSpacing : CGFloat ? = nil
212
396
#endif
213
397
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
214
413
@available ( iOS 15 , visionOS 1 . 0 , * )
215
414
struct MockBadgeView : View {
216
415
var body : some View {
@@ -237,6 +436,7 @@ struct MockBadgeView: View {
237
436
#endif
238
437
}
239
438
}
439
+ #endif
240
440
241
441
#if DEBUG
242
442
@available ( iOS 15 , visionOS 1 . 0 , * )
@@ -248,3 +448,4 @@ struct ConsoleTaskCell_Previews: PreviewProvider {
248
448
}
249
449
}
250
450
#endif
451
+
0 commit comments