krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.8.0: 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}