GiteaPR: native iOS app to manage Gitea pull requests
Build iOS App / Build .ipa and publish source (push) Failing after 35s
Build iOS App / Build .ipa and publish source (push) Failing after 35s
Multi-repo PR inbox, review (approve/request changes/comment), merge with gating toggle, read-only diff viewer, and Gitea Actions check status with one-tap re-run. Ships unsigned via the iMac runner to the SideStore source.
This commit is contained in:
commit
1025634978
27 files changed
+2322
No files matched your search
@@ -0,0 +1,258 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user