krz/hutch

an ios client for sourcehut

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

v2.4.1: Hutch/Views/Builds/BuildTaskLogView.swift · raw

  1import SwiftUI
  2import UIKit
  3
  4struct BuildTaskLogView: View {
  5    let taskName: String
  6    let viewModel: BuildDetailViewModel
  7
  8    /// Always reflects the latest version of the task from the live job data.
  9    private var task: BuildTask? {
 10        viewModel.job?.tasks.first(where: { $0.name == taskName })
 11    }
 12
 13    var body: some View {
 14        Group {
 15            if let task {
 16                if let logText = viewModel.displayedLogText(for: task) {
 17                    BuildLogTextView(text: logText)
 18                    .safeAreaInset(edge: .bottom) {
 19                        if viewModel.isShowingBuildLogFallback(for: task) {
 20                            Text("Showing the live build log until a task-specific log is available.")
 21                                .font(.footnote)
 22                                .foregroundStyle(.secondary)
 23                                .frame(maxWidth: .infinity, alignment: .leading)
 24                                .padding(.horizontal)
 25                                .padding(.vertical, 8)
 26                                .background(.thinMaterial)
 27                        }
 28                    }
 29                    .toolbar {
 30                        ToolbarItem(placement: .topBarTrailing) {
 31                            ShareLink(item: logText)
 32                        }
 33                        ToolbarItem(placement: .topBarTrailing) {
 34                            Button {
 35                                UIPasteboard.general.string = logText
 36                            } label: {
 37                                Image(systemName: "doc.on.doc")
 38                            }
 39                            .accessibilityLabel("Copy log to clipboard")
 40                        }
 41                    }
 42                } else if viewModel.loadingTaskLogs.contains(task.logCacheKey) {
 43                    SRHTLoadingStateView(message: "Loading log…")
 44                } else if task.log == nil {
 45                    if task.status == .running || task.status == .pending {
 46                        if viewModel.isLoadingBuildLog {
 47                            SRHTLoadingStateView(message: "Loading build log…")
 48                        } else {
 49                            SRHTLoadingStateView(message: "Waiting for log…")
 50                        }
 51                    } else {
 52                        ContentUnavailableView(
 53                            "No Log",
 54                            systemImage: "doc.text",
 55                            description: Text("This task has no log output.")
 56                        )
 57                    }
 58                } else if viewModel.failedTaskLogs.contains(task.logCacheKey) {
 59                    ContentUnavailableView {
 60                        Label("Couldn't Load Log", systemImage: "exclamationmark.triangle")
 61                    } description: {
 62                        Text("The log is temporarily unavailable. Retry to fetch the latest output.")
 63                    } actions: {
 64                        Button("Retry") {
 65                            Task {
 66                                await viewModel.retryTaskLog(task: task)
 67                            }
 68                        }
 69                    }
 70                } else {
 71                    SRHTLoadingStateView(message: "Loading log…")
 72                }
 73            } else {
 74                SRHTLoadingStateView(message: "Loading log…")
 75            }
 76        }
 77        .navigationTitle(taskName)
 78        .navigationBarTitleDisplayMode(.inline)
 79        // Re-runs whenever the log URL changes or a retry is requested.
 80        .task(id: viewModel.taskLogTrigger(for: task)) {
 81            guard let task else { return }
 82            await viewModel.loadTaskLog(task: task)
 83        }
 84        .task(id: viewModel.job?.log?.fullURL) {
 85            guard let task else { return }
 86            guard task.status == .running || task.status == .pending else { return }
 87            await viewModel.loadBuildLog()
 88        }
 89    }
 90}
 91
 92private struct BuildLogTextView: UIViewRepresentable {
 93    let text: String
 94
 95    func makeUIView(context _: Context) -> UITextView {
 96        let textView = UITextView()
 97        textView.isEditable = false
 98        textView.isSelectable = true
 99        textView.isScrollEnabled = true
100        textView.alwaysBounceVertical = true
101        textView.alwaysBounceHorizontal = true
102        textView.showsHorizontalScrollIndicator = true
103        textView.showsVerticalScrollIndicator = true
104        textView.backgroundColor = .clear
105        textView.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
106        textView.textContainer.lineFragmentPadding = 0
107        textView.textContainer.widthTracksTextView = false
108        textView.font = UIFont.monospacedSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .caption2).pointSize, weight: .regular)
109        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
110        return textView
111    }
112
113    func updateUIView(_ uiView: UITextView, context _: Context) {
114        guard uiView.text != text else { return }
115        uiView.text = text
116    }
117}