krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.1.4: Hutch/Views/Builds/BuildRowView.swift · raw
1import SwiftUI
2
3struct BuildRowView: View, Equatable {
4 let job: JobSummary
5
6 var body: some View {
7 HStack(spacing: 12) {
8 JobStatusIcon(status: job.status)
9 .frame(width: 28)
10
11 VStack(alignment: .leading, spacing: 4) {
12 Text(job.displayLabel)
13 .font(.subheadline)
14 .lineLimit(1)
15
16 HStack(spacing: 8) {
17 if let image = job.image {
18 Text(image)
19 .font(.caption)
20 .foregroundStyle(.secondary)
21 }
22
23 Spacer()
24
25 Text(job.created.relativeDescription)
26 .font(.caption)
27 .foregroundStyle(.tertiary)
28 }
29
30 if !job.tasks.isEmpty {
31 TaskProgressView(tasks: job.tasks)
32 }
33 }
34 }
35 .padding(.vertical, 2)
36 }
37}
38
39struct JobStatusBadge: View {
40 let status: JobStatus
41
42 var body: some View {
43 HStack(spacing: 4) {
44 JobStatusIcon(status: status)
45 .frame(width: 12, height: 12)
46 Text(status.displayTitle)
47 .font(.caption2.weight(.medium))
48 }
49 .foregroundStyle(.secondary)
50 .padding(.horizontal, 8)
51 .padding(.vertical, 4)
52 .background(Color(.secondarySystemFill), in: Capsule())
53 }
54}
55
56extension JobStatus {
57 var displayTitle: String {
58 switch self {
59 case .pending:
60 "Pending"
61 case .queued:
62 "Queued"
63 case .running:
64 "Running"
65 case .success:
66 "Succeeded"
67 case .failed:
68 "Failed"
69 case .cancelled:
70 "Cancelled"
71 case .timeout:
72 "Timed Out"
73 }
74 }
75}
76
77// MARK: - Job Status Icon
78
79struct JobStatusIcon: View {
80 let status: JobStatus
81
82 var body: some View {
83 Image(systemName: iconName)
84 .foregroundStyle(color)
85 .symbolEffect(.pulse, isActive: status == .running)
86 }
87
88 private var iconName: String {
89 switch status {
90 case .success: "checkmark.circle.fill"
91 case .failed, .timeout: "xmark.circle.fill"
92 case .running: "arrow.trianglehead.2.clockwise.rotate.90"
93 case .queued: "clock.fill"
94 case .pending: "circle.dashed"
95 case .cancelled: "minus.circle.fill"
96 }
97 }
98
99 private var color: Color {
100 switch status {
101 case .success: .green
102 case .failed, .timeout: .red
103 case .running: .yellow
104 case .queued: .orange
105 case .pending, .cancelled: .gray
106 }
107 }
108}
109
110// MARK: - Task Progress
111
112struct TaskProgressView: View {
113 let tasks: [JobTaskSummary]
114
115 var body: some View {
116 HStack(spacing: 6) {
117 ProgressView(value: progress, total: 1.0)
118 .tint(progressColor)
119 .frame(maxWidth: 80)
120
121 Text("\(completedCount)/\(tasks.count) tasks")
122 .font(.caption2)
123 .foregroundStyle(.secondary)
124 }
125 }
126
127 private var completedCount: Int {
128 tasks.filter { $0.status == .success }.count
129 }
130
131 private var progress: Double {
132 tasks.isEmpty ? 0 : Double(completedCount) / Double(tasks.count)
133 }
134
135 private var progressColor: Color {
136 if tasks.contains(where: { $0.status == .failed }) {
137 return .red
138 }
139 if completedCount == tasks.count {
140 return .green
141 }
142 return .blue
143 }
144}