Files
ios-gitea-prs/GiteaPR/Detail/Tabs.swift
T
Hermes de9729b898
Build iOS App / Build .ipa and publish source (push) Successful in 23s
Hide Logs button for unrun jobs; remove inbox filter bar; bump to 1.8
- Checks: only show the Logs button once a job has actually run (terminal
  conclusion). Queued/running jobs are grey with no logs, so the button was
  useless there.
- Inbox: remove the segmented filter bar (All/Assigned/By me/Review) — not
  useful yet. Bump to 1.8.
2026-08-12 22:06:09 +00:00

279 lines
9.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(repo: vm.repo, prNumber: vm.prNumber, 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)
}
if hasRun(job) {
NavigationLink {
JobLogsView(repo: vm.repo, runID: run.id, jobID: job.id,
jobName: job.name)
} label: {
Text("Logs")
.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"
}
/// A job has run only once it has a terminal conclusion. Queued/running
/// jobs (grey, no logs yet) hide the Logs button.
private func hasRun(_ job: ActionJob) -> Bool {
let s = job.status.lowercased()
if s == "queued" || s == "running" || s == "waiting" || s == "requested" {
return false
}
return (job.conclusion ?? "").lowercased() != ""
}
}