import SwiftUI struct PRDetailView: View { @StateObject private var vm: PRDetailViewModel @State private var selectedTab: DetailTab = .overview @State private var showCommentSheet = false @State private var showMergeSheet = false @State private var showCloseConfirm = false @State private var showRerunConfirm: ActionRun? @State private var requestChangesMode = false enum DetailTab: String, CaseIterable, Identifiable { case overview = "Overview" case conversation = "Conversation" case files = "Files" case checks = "Checks" var id: String { rawValue } var icon: String { switch self { case .overview: return "doc.text" case .conversation: return "bubble.left.and.bubble.right" case .files: return "doc.plaintext" case .checks: return "checkmark.circle" } } } init(repo: Repository, prNumber: Int) { _vm = StateObject(wrappedValue: PRDetailViewModel(repo: repo, prNumber: prNumber)) } var body: some View { Group { if let pr = vm.pr { content(pr) } else if vm.isLoading { ProgressView("Loading…") } else if let err = vm.errorMessage { ContentUnavailableView("Couldn't load PR", systemImage: "exclamationmark.triangle", description: Text(err)) .toolbar { ToolbarItem(placement: .primaryAction) { reloadButton } } } else { ProgressView("Loading…") } } .navigationTitle("#\(vm.prNumber)") .navigationBarTitleDisplayMode(.inline) .task { await vm.load() } .alert("Error", isPresented: .init( get: { vm.errorMessage != nil }, set: { if !$0 { vm.errorMessage = nil } })) { Button("OK", role: .cancel) {} } message: { Text(vm.errorMessage ?? "") } .overlay { if vm.busyMessage != nil { busyOverlay } } .confirmationDialog("Close this PR?", isPresented: $showCloseConfirm, titleVisibility: .visible) { Button("Close", role: .destructive) { Task { await vm.setState("closed") } } Button("Cancel", role: .cancel) {} } message: { Text("This will close the pull request.") } .sheet(isPresented: $showCommentSheet) { CommentSheet(vm: vm, mode: requestChangesMode ? .requestChanges : .comment) .onDisappear { requestChangesMode = false } } .sheet(isPresented: $showMergeSheet) { MergeSheet(vm: vm) } .confirmationDialog("Re-run workflow?", isPresented: .init( get: { showRerunConfirm != nil }, set: { if !$0 { showRerunConfirm = nil } }), titleVisibility: .visible) { Button("Re-run") { if let run = showRerunConfirm { Task { await vm.rerun(runID: run.id) } } showRerunConfirm = nil } Button("Cancel", role: .cancel) {} } message: { Text("Re-run \"\(showRerunConfirm?.name ?? "workflow")\"?") } } @ViewBuilder private func content(_ pr: PullRequest) -> some View { VStack(spacing: 0) { header(pr) ScrollView { switch selectedTab { case .overview: OverviewTab(pr: pr) case .conversation: ConversationTab(vm: vm) case .files: FilesTab(vm: vm) case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 }) } } bottomTabBar } .toolbar { if prIsOpen { ToolbarItem(placement: .primaryAction) { mergeButton } ToolbarItem(placement: .primaryAction) { reviewMenu } } } } @ViewBuilder private func header(_ pr: PullRequest) -> some View { VStack(alignment: .leading, spacing: 8) { Text(pr.title) .font(.title3.weight(.bold)) HStack(spacing: 6) { Text("\(vm.repo.fullName)#\(pr.number)") .font(.caption) .foregroundColor(.secondary) CICircle(status: vm.ciStatus) if vm.reviewState != .unknown { ReviewBadge(state: vm.reviewState) } if pr.draft == true { Text("Draft").font(.caption2).foregroundColor(.secondary) } } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color(.secondarySystemBackground)) } /// Bottom tab bar: full-width frosted-glass bar at the standard iOS tab /// bar height, sitting flush above the home indicator — matching Apple /// Music's tab bar (not a floating capsule with space below it). private var bottomTabBar: some View { HStack { ForEach(DetailTab.allCases) { tab in Button { selectedTab = tab } label: { VStack(spacing: 2) { Image(systemName: tab.icon) .font(.system(size: 22)) Text(tab.rawValue) .font(.caption2) } .foregroundColor(selectedTab == tab ? .accentColor : .secondary) .frame(maxWidth: .infinity) } } } .padding(.top, 8) .padding(.bottom, 4) .background(.bar) } private var reviewMenu: some View { Menu { Button("Comment") { requestChangesMode = false; showCommentSheet = true } Button("Approve") { Task { await vm.submitReview(state: "approved", comment: nil) } } Button("Request changes") { requestChangesMode = true; showCommentSheet = true } Divider() if prIsOpen { Button("Close", role: .destructive) { showCloseConfirm = true } } else { Button("Reopen") { Task { await vm.setState("open") } } } } label: { Image(systemName: "checkmark.bubble") } } private var mergeButton: some View { Button { showMergeSheet = true } label: { Image(systemName: "arrow.triangle.merge") } .disabled(vm.ciStatus == .failure && SettingsStore.shared.mergeGateMode == .block) } private var prIsOpen: Bool { (vm.pr?.state.lowercased() ?? "") == "open" } private var reloadButton: some View { Button { Task { await vm.load() } } label: { Image(systemName: "arrow.clockwise") } } private var busyOverlay: some View { ZStack { Color.black.opacity(0.15) ProgressView(vm.busyMessage ?? "") .padding() .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) } .ignoresSafeArea() } }