import SwiftUI // MARK: - Conversation struct ConversationTab: View { @ObservedObject var vm: PRDetailViewModel var body: some View { VStack(alignment: .leading, spacing: 12) { if vm.reviews.count > 0 { Text("Reviews") .font(.headline) ForEach(vm.reviews) { review in reviewRow(review) } } Text("Comments") .font(.headline) if vm.comments.isEmpty { Text("No comments yet.") .font(.callout) .foregroundColor(.secondary) } ForEach(vm.comments) { comment in commentRow(comment) } Text("Inline file comments are not shown — the Gitea API does not expose them.") .font(.caption2) .foregroundColor(.secondary) .padding(.top, 8) } .frame(maxWidth: .infinity, alignment: .leading) .padding() } private func reviewRow(_ review: PullReview) -> some View { HStack(alignment: .top, spacing: 8) { Text(icon) VStack(alignment: .leading, spacing: 2) { Text("\(review.user?.login ?? "unknown") \(verb)") .font(.subheadline.weight(.semibold)) if let body = review.body, !body.isEmpty { Text(body).font(.callout) } } Spacer() } } private func commentRow(_ comment: IssueComment) -> some View { VStack(alignment: .leading, spacing: 4) { HStack { Text(comment.user?.login ?? "unknown") .font(.subheadline.weight(.semibold)) if let created = comment.createdAt { Text(created, style: .relative) .font(.caption2) .foregroundColor(.secondary) } } if let body = comment.body, !body.isEmpty { Text(body).font(.callout) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(8) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 8)) } private var icon: String { switch vm.reviewState { case .approved: return "✅" case .changesRequested: return "🔴" default: return "💬" } } private var verb: String { switch vm.reviewState { case .approved: return "approved" case .changesRequested: return "requested changes" default: return "reviewed" } } } // MARK: - Files struct FilesTab: View { @ObservedObject var vm: PRDetailViewModel var body: some View { if vm.files.isEmpty { Text("No changed files.") .font(.callout) .foregroundColor(.secondary) .frame(maxWidth: .infinity) .padding() } else { LazyVStack(spacing: 0) { ForEach(vm.files) { file in NavigationLink(value: file) { fileRow(file) } Divider() } } .navigationDestination(for: ChangedFile.self) { file in DiffView(file: file) } } } private func fileRow(_ file: ChangedFile) -> some View { VStack(alignment: .leading, spacing: 4) { HStack { Text(statusIcon(file)) Text(file.filename) .font(.body) .lineLimit(1) Spacer() } HStack(spacing: 8) { Text("+\(file.additions)").foregroundColor(.green) .font(.caption2.monospacedDigit()) Text("−\(file.deletions)").foregroundColor(.red) .font(.caption2.monospacedDigit()) Spacer() } } .padding(.vertical, 6) .padding(.horizontal) } private func statusIcon(_ file: ChangedFile) -> String { switch file.status ?? "" { case "added": return "+" case "removed": return "-" case "renamed": return "→" default: return "✎" } } } // MARK: - Commits struct CommitsTab: View { let commits: [PRCommit] var body: some View { VStack(alignment: .leading, spacing: 10) { ForEach(commits) { commit in HStack(alignment: .top, spacing: 10) { Text(commit.sha ?? commit.id) .font(.caption2.monospaced()) .foregroundColor(.secondary) .frame(width: 60, alignment: .leading) VStack(alignment: .leading, spacing: 2) { Text(commit.commit?.message ?? "(no message)") .font(.callout) .lineLimit(3) Text(commit.author?.name ?? "") .font(.caption2) .foregroundColor(.secondary) } } Divider() } } .frame(maxWidth: .infinity, alignment: .leading) .padding() } } // MARK: - Checks struct ChecksTab: View { @ObservedObject var vm: PRDetailViewModel let onRerun: (ActionRun) -> Void var body: some View { VStack(alignment: .leading, spacing: 10) { if vm.runs.isEmpty { Text("No workflow runs for this head commit.") .font(.callout) .foregroundColor(.secondary) } ForEach(vm.runs) { run in runCard(run) } } .frame(maxWidth: .infinity, alignment: .leading) .padding() } private func runCard(_ run: ActionRun) -> some View { VStack(alignment: .leading, spacing: 8) { HStack { CICircle(status: statusFor(run)) Text(run.name ?? "workflow") .font(.subheadline.weight(.semibold)) Spacer() if let started = run.createdAt { Text(started, style: .relative) .font(.caption2) .foregroundColor(.secondary) } } if let jobs = vm.jobs[run.id], !jobs.isEmpty { ForEach(jobs) { job in HStack { CICircle(status: statusFor(job)) Text(job.name).font(.caption) Spacer() if isFailed(job) { Button("Re-run") { onRerun(run) } .font(.caption) .buttonStyle(.bordered) } } } } } .padding(10) .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 10)) } private func statusFor(_ run: ActionRun) -> CIStatus { let c = (run.conclusion ?? "").lowercased() let s = (run.status ?? "").lowercased() if s == "running" || s == "queued" || s == "waiting" { return .running } switch c { case "success": return .success case "failure": return .failure case "cancelled": return .cancelled default: return .unknown } } private func statusFor(_ job: ActionJob) -> CIStatus { let c = (job.conclusion ?? "").lowercased() let s = job.status.lowercased() if s == "running" || s == "queued" { return .running } switch c { case "success": return .success case "failure": return .failure case "cancelled": return .cancelled default: return .unknown } } private func isFailed(_ job: ActionJob) -> Bool { let c = (job.conclusion ?? "").lowercased() return c == "failure" || c == "cancelled" } }