import SwiftUI /// A build: what it was, then what it printed. struct BuildDetailView: View { @State private var model: BuildDetailViewModel init(client: GitbayClient, repo: String, number: Int64) { _model = State(initialValue: BuildDetailViewModel( client: client, repoPath: repo, number: number )) } var body: some View { ZStack { Color.clear if let detail = model.state.value { VStack(spacing: 0) { header(detail.build) Divider() log(detail.log) } } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle("Build #\(model.number)") .navigationBarTitleDisplayMode(.inline) .task { await model.load() } .refreshable { await model.load() } } private func header(_ build: Build) -> some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Text(build.job) .font(.gbSans(.subheadline).weight(.medium)) GBChip(build.status, build.statusColor) Spacer() } HStack(spacing: 6) { Text(build.ref) Text("·") NavigationLink(value: RepoRoute.commit(repo: model.repoPath, sha: build.sha)) { Text(build.shortSHA).font(.gbMono(.caption)) } } .font(.gbSans(.caption)) .foregroundStyle(.secondary) Text(timing(build)) .font(.gbSans(.caption)) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) .padding(.vertical, 12) } /// Queued always; finished only once it is. private func timing(_ build: Build) -> String { let queued = build.createdAt.formatted(date: .abbreviated, time: .shortened) guard let finished = build.finishedAt else { return "queued \(queued)" } return "queued \(queued) · finished \(finished.formatted(date: .omitted, time: .shortened))" } @ViewBuilder private func log(_ text: String) -> some View { if text.isEmpty { ContentUnavailableView { Label("No log yet", systemImage: "doc.text") } } else { ScrollView([.horizontal, .vertical]) { Text(text) .font(.gbMono(.caption2)) .frame(maxWidth: .infinity, alignment: .leading) .padding(12) .textSelection(.enabled) } } } }