diff options
| author | Christian Cleberg <[email protected]> | 2026-03-17 23:19:43 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-17 23:19:43 -0500 |
| commit | 32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5 (patch) | |
| tree | ee36d421d704e508d5617d803b4c8cbdb4804fe1 /Hutch/Views/Builds/BuildListView.swift | |
| parent | 8f2057c53e9009c2529c9c4849c914666c0e4b40 (diff) | |
| download | hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.gz hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.bz2 hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.zip | |
v1.0
Diffstat (limited to 'Hutch/Views/Builds/BuildListView.swift')
| -rw-r--r-- | Hutch/Views/Builds/BuildListView.swift | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift new file mode 100644 index 0000000..875a836 --- /dev/null +++ b/Hutch/Views/Builds/BuildListView.swift @@ -0,0 +1,217 @@ +import SwiftUI + +struct BuildListView: View { + @Environment(AppState.self) private var appState + @State private var viewModel: BuildListViewModel? + @State private var showSubmitSheet = false + @State private var submittedJobId: Int? + + var body: some View { + Group { + if let viewModel { + listContent(viewModel) + } else { + SRHTLoadingStateView(message: "Loading builds…") + } + } + .navigationTitle("Builds") + .toolbar { + if viewModel != nil { + ToolbarItem(placement: .topBarTrailing) { + Button { + showSubmitSheet = true + } label: { + Image(systemName: "plus") + } + } + } + } + .sheet(isPresented: $showSubmitSheet) { + if let viewModel { + SubmitBuildSheet(viewModel: viewModel) { jobId in + showSubmitSheet = false + submittedJobId = jobId + } + } + } + .navigationDestination(for: JobSummary.self) { job in + BuildDetailView(jobId: job.id) + } + .navigationDestination(isPresented: Binding( + get: { submittedJobId != nil }, + set: { isPresented in + if !isPresented { + submittedJobId = nil + } + } + )) { + if let submittedJobId { + BuildDetailView(jobId: submittedJobId) + } + } + .task { + if viewModel == nil { + let vm = BuildListViewModel(client: appState.client) + viewModel = vm + await vm.loadJobs() + } + } + } + + @ViewBuilder + private func listContent(_ viewModel: BuildListViewModel) -> some View { + @Bindable var vm = viewModel + + List { + ForEach(viewModel.jobs) { job in + NavigationLink(value: job) { + BuildRowView(job: job) + } + .task { + await viewModel.loadMoreIfNeeded(currentItem: job) + } + } + + if viewModel.isLoadingMore { + HStack { + Spacer() + ProgressView() + Spacer() + } + .listRowSeparator(.hidden) + } + } + .listStyle(.plain) + .overlay { + if viewModel.isLoading, viewModel.jobs.isEmpty { + SRHTLoadingStateView(message: "Loading builds…") + } else if let error = viewModel.error, viewModel.jobs.isEmpty { + SRHTErrorStateView( + title: "Couldn't Load Builds", + message: error, + retryAction: { await viewModel.loadJobs() } + ) + } else if viewModel.jobs.isEmpty, viewModel.error == nil { + ContentUnavailableView( + "No Builds", + systemImage: "hammer", + description: Text("Your build jobs will appear here.") + ) + } + } + .connectivityOverlay(hasContent: !viewModel.jobs.isEmpty) { + await viewModel.loadJobs() + } + .srhtErrorBanner(error: $vm.error) + .refreshable { + await viewModel.loadJobs() + } + } +} + +private struct SubmitBuildSheet: View { + let viewModel: BuildListViewModel + let onSubmitted: (Int) -> Void + + @Environment(\.dismiss) private var dismiss + @Bindable var viewModelBindable: BuildListViewModel + @State private var manifest = "" + @State private var tagsText = "" + @State private var note = "" + @State private var secrets = false + @State private var execute = true + @State private var visibility: Visibility = .public + + init(viewModel: BuildListViewModel, onSubmitted: @escaping (Int) -> Void) { + self.viewModel = viewModel + self._viewModelBindable = Bindable(viewModel) + self.onSubmitted = onSubmitted + } + + var body: some View { + NavigationStack { + Form { + Section("Build Manifest") { + TextField("Paste a build manifest", text: $manifest, axis: .vertical) + .font(.system(.body, design: .monospaced)) + .lineLimit(12...24) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } + + Section("Build Options") { + TextField("Note (optional)", text: $note) + TextField("Tags (comma-separated, optional)", text: $tagsText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + Picker("Visibility", selection: $visibility) { + Text("Public").tag(Visibility.public) + Text("Unlisted").tag(Visibility.unlisted) + Text("Private").tag(Visibility.private) + } + Toggle("Start build now", isOn: $execute) + Toggle("Allow build secrets", isOn: $secrets) + } + + Section { + Text("You need a valid builds.sr.ht manifest and a token with BUILDS:RW.") + .font(.footnote) + .foregroundStyle(.secondary) + } + + if let error = viewModel.error { + Section { + Label { + Text(error) + } icon: { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.red) + } + .foregroundStyle(.red) + } + } + } + .navigationTitle("Submit Build") + .navigationBarTitleDisplayMode(.inline) + .onDisappear { + viewModelBindable.error = nil + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + viewModelBindable.error = nil + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button { + Task { + let tags = tagsText + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + if let jobId = await viewModel.submitBuild( + manifest: manifest, + tags: tags, + note: note, + secrets: secrets, + execute: execute, + visibility: visibility + ) { + onSubmitted(jobId) + } + } + } label: { + if viewModel.isSubmitting { + ProgressView() + .controlSize(.small) + } else { + Text("Submit Build") + } + } + .disabled(manifest.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSubmitting) + } + } + } + } +} |
