Commit bc351b2fcd

bc351b2fcd8cd4435cb1739108c998323b4787d4

parent: bc2dcb7e82

Unsigned

cmc <hello@cleberg.net> · 2026-03-23 01:40 UTC

feat: replace inline task log expansion with a full-screen log view

Layout: unified · split

Hutch/Views/Builds/BuildDetailView.swift +27 −55
@@ -6,6 +6,7 @@ struct BuildDetailView: View {
66 @Environment(AppState.self) private var appState
77 @State private var viewModel: BuildDetailViewModel?
88 @State private var rebuiltJobId: Int?
9 @State private var selectedTask: BuildTask?
910 @State private var showEditResubmitSheet = false
1011 @State private var showCancelConfirmation = false
1112
@@ -41,6 +42,18 @@ struct BuildDetailView: View {
4142 BuildDetailView(jobId: rebuiltJobId)
4243 }
4344 }
45 .navigationDestination(isPresented: Binding(
46 get: { selectedTask != nil },
47 set: { isPresented in
48 if !isPresented {
49 selectedTask = nil
50 }
51 }
52 )) {
53 if let selectedTask, let viewModel {
54 BuildTaskLogView(task: selectedTask, viewModel: viewModel)
55 }
56 }
4457 .sheet(isPresented: $showEditResubmitSheet) {
4558 if let viewModel, let job = viewModel.job {
4659 EditResubmitBuildSheet(viewModel: viewModel, job: job) { jobId in
@@ -115,7 +128,20 @@ struct BuildDetailView: View {
115128 if !job.tasks.isEmpty {
116129 ForEach(job.tasks) { task in
117130 Section {
118 TaskLogSection(task: task, viewModel: viewModel)
131 Button {
132 selectedTask = task
133 } label: {
134 HStack {
135 Text(task.status.rawValue.capitalized)
136 .font(.subheadline)
137 .foregroundStyle(.secondary)
138 Spacer()
139 Image(systemName: "chevron.right")
140 .font(.caption)
141 .foregroundStyle(.tertiary)
142 }
143 }
144 .foregroundStyle(.primary)
119145 } header: {
120146 HStack(spacing: 6) {
121147 TaskStatusIcon(status: task.status)
@@ -296,60 +322,6 @@ private struct EditResubmitBuildSheet: View {
296322 }
297323}
298324
299// MARK: - Task Log Section
300
301private struct TaskLogSection: View {
302 let task: BuildTask
303 let viewModel: BuildDetailViewModel
304
305 @State private var isExpanded: Bool
306
307 init(task: BuildTask, viewModel: BuildDetailViewModel) {
308 self.task = task
309 self.viewModel = viewModel
310 self._isExpanded = State(initialValue: task.status == .failed)
311 }
312
313 var body: some View {
314 DisclosureGroup(isExpanded: $isExpanded) {
315 if viewModel.loadingTaskLogs.contains(task.logCacheKey) {
316 HStack {
317 Spacer()
318 ProgressView("Loading log…")
319 Spacer()
320 }
321 } else if let logText = viewModel.taskLogs[task.logCacheKey] {
322 ScrollView(.horizontal, showsIndicators: false) {
323 Text(logText)
324 .font(.caption2.monospaced())
325 .foregroundStyle(.primary)
326 .textSelection(.enabled)
327 .frame(maxWidth: .infinity, alignment: .leading)
328 }
329 } else if task.log == nil {
330 Text("No log available.")
331 .foregroundStyle(.secondary)
332 }
333 } label: {
334 HStack {
335 Text(task.status.rawValue)
336 .font(.caption)
337 .foregroundStyle(.secondary)
338 }
339 }
340 .task {
341 if isExpanded {
342 await viewModel.loadTaskLog(task: task)
343 }
344 }
345 .onChange(of: isExpanded) { _, expanded in
346 if expanded {
347 Task { await viewModel.loadTaskLog(task: task) }
348 }
349 }
350 }
351}
352
353325// MARK: - Task Status Icon
354326
355327private struct TaskStatusIcon: View {
Hutch/Views/Builds/BuildTaskLogView.swift added +55
@@ -0,0 +1,55 @@
1import SwiftUI
2import UIKit
3
4struct BuildTaskLogView: View {
5 let task: BuildTask
6 let viewModel: BuildDetailViewModel
7
8 var body: some View {
9 Group {
10 if viewModel.loadingTaskLogs.contains(task.logCacheKey) {
11 SRHTLoadingStateView(message: "Loading log…")
12 } else if let logText = viewModel.taskLogs[task.logCacheKey] {
13 GeometryReader { geometry in
14 ScrollView([.vertical, .horizontal]) {
15 Text(logText)
16 .font(.caption2.monospaced())
17 .textSelection(.enabled)
18 .padding()
19 .frame(
20 minWidth: geometry.size.width,
21 minHeight: geometry.size.height,
22 alignment: .topLeading
23 )
24 }
25 }
26 .toolbar {
27 ToolbarItem(placement: .topBarTrailing) {
28 ShareLink(item: logText)
29 }
30 ToolbarItem(placement: .topBarTrailing) {
31 Button {
32 UIPasteboard.general.string = logText
33 } label: {
34 Image(systemName: "doc.on.doc")
35 }
36 .accessibilityLabel("Copy log to clipboard")
37 }
38 }
39 } else if task.log == nil {
40 ContentUnavailableView(
41 "No Log",
42 systemImage: "doc.text",
43 description: Text("This task has no log output.")
44 )
45 } else {
46 SRHTLoadingStateView(message: "Loading log…")
47 }
48 }
49 .navigationTitle(task.name)
50 .navigationBarTitleDisplayMode(.inline)
51 .task {
52 await viewModel.loadTaskLog(task: task)
53 }
54 }
55}