Restructure PR detail: remove Commits tab, bottom icon tab bar, toolbar actions, markdown overview; bump to 1.9
Build iOS App / Build .ipa and publish source (push) Successful in 21s

- Remove the Commits tab (not useful).
- Move the section switcher to a bottom tab bar with icons (Overview /
  Conversation / Files / Checks) instead of a top segmented control.
- Move Review (menu) and Merge into the navigation toolbar, removing the
  bottom action bar.
- Add an Overview tab that renders the PR body as markdown via SwiftUI's
  built-in AttributedString(markdown:) (no third-party dependency).
- Bump to 1.9.
This commit is contained in:
Hermes committed 2026-08-12 22:28:18 +00:00
1 parent de9729b898
commit fce0385708
3 files changed
+100 -66

No files matched your search

+4 -4
View File
@@ -187,7 +187,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 9; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
@@ -197,7 +197,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.8; MARKETING_VERSION = 1.9;
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -211,7 +211,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 9; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
@@ -221,7 +221,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.8; MARKETING_VERSION = 1.9;
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
+60 -42
View File
@@ -2,8 +2,7 @@ import SwiftUI
struct PRDetailView: View { struct PRDetailView: View {
@StateObject private var vm: PRDetailViewModel @StateObject private var vm: PRDetailViewModel
@State private var selectedTab: DetailTab = .conversation @State private var selectedTab: DetailTab = .overview
@State private var showActionSheet = false
@State private var showCommentSheet = false @State private var showCommentSheet = false
@State private var showMergeSheet = false @State private var showMergeSheet = false
@State private var showCloseConfirm = false @State private var showCloseConfirm = false
@@ -11,11 +10,21 @@ struct PRDetailView: View {
@State private var requestChangesMode = false @State private var requestChangesMode = false
enum DetailTab: String, CaseIterable, Identifiable { enum DetailTab: String, CaseIterable, Identifiable {
case overview = "Overview"
case conversation = "Conversation" case conversation = "Conversation"
case files = "Files" case files = "Files"
case commits = "Commits"
case checks = "Checks" case checks = "Checks"
var id: String { rawValue } 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) { init(repo: Repository, prNumber: Int) {
@@ -47,17 +56,6 @@ struct PRDetailView: View {
Button("OK", role: .cancel) {} Button("OK", role: .cancel) {}
} message: { Text(vm.errorMessage ?? "") } } message: { Text(vm.errorMessage ?? "") }
.overlay { if vm.busyMessage != nil { busyOverlay } } .overlay { if vm.busyMessage != nil { busyOverlay } }
.confirmationDialog("Actions", isPresented: $showActionSheet, titleVisibility: .visible) {
Button("Comment") { showCommentSheet = true }
Button("Approve") { Task { await vm.submitReview(state: "approved", comment: nil) } }
Button("Request changes") { requestChangesMode = true; showCommentSheet = true }
Button("Merge") { showMergeSheet = true }
if prIsOpen {
Button("Close", role: .destructive) { showCloseConfirm = true }
} else {
Button("Reopen") { Task { await vm.setState("open") } }
}
}
.confirmationDialog("Close this PR?", isPresented: $showCloseConfirm, titleVisibility: .visible) { .confirmationDialog("Close this PR?", isPresented: $showCloseConfirm, titleVisibility: .visible) {
Button("Close", role: .destructive) { Task { await vm.setState("closed") } } Button("Close", role: .destructive) { Task { await vm.setState("closed") } }
Button("Cancel", role: .cancel) {} Button("Cancel", role: .cancel) {}
@@ -89,28 +87,24 @@ struct PRDetailView: View {
private func content(_ pr: PullRequest) -> some View { private func content(_ pr: PullRequest) -> some View {
VStack(spacing: 0) { VStack(spacing: 0) {
header(pr) header(pr)
Picker("Tab", selection: $selectedTab) {
ForEach(DetailTab.allCases) { t in Text(t.rawValue).tag(t) }
}
.pickerStyle(.segmented)
.padding(.horizontal)
.padding(.bottom, 8)
ScrollView { ScrollView {
switch selectedTab { switch selectedTab {
case .overview: OverviewTab(pr: pr)
case .conversation: ConversationTab(vm: vm) case .conversation: ConversationTab(vm: vm)
case .files: FilesTab(vm: vm) case .files: FilesTab(vm: vm)
case .commits: CommitsTab(commits: vm.commits)
case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 }) case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 })
} }
} }
if prIsOpen { bottomTabBar
actionBar(pr)
}
} }
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { reloadButton } ToolbarItem(placement: .primaryAction) { reloadButton }
if prIsOpen {
ToolbarItem(placement: .primaryAction) { mergeButton }
ToolbarItem(placement: .primaryAction) { reviewMenu }
}
} }
} }
@@ -131,36 +125,60 @@ struct PRDetailView: View {
Text("Draft").font(.caption2).foregroundColor(.secondary) Text("Draft").font(.caption2).foregroundColor(.secondary)
} }
} }
if let body = pr.body, !body.isEmpty {
Text(body)
.font(.callout)
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemBackground)) .background(Color(.secondarySystemBackground))
} }
@ViewBuilder /// Bottom tab bar: icon-only navigation between the PR's sections.
private func actionBar(_ pr: PullRequest) -> some View { private var bottomTabBar: some View {
HStack { HStack {
Button { showActionSheet = true } label: { ForEach(DetailTab.allCases) { tab in
Label("Review", systemImage: "checkmark.bubble") Button {
selectedTab = tab
} label: {
VStack(spacing: 2) {
Image(systemName: tab.icon)
.font(.system(size: 20))
Text(tab.rawValue)
.font(.caption2)
}
.foregroundColor(selectedTab == tab ? .accentColor : .secondary)
.frame(maxWidth: .infinity)
}
} }
.buttonStyle(.borderedProminent)
Button("Merge") { showMergeSheet = true }
.buttonStyle(.bordered)
.disabled(vm.ciStatus == .failure &&
SettingsStore.shared.mergeGateMode == .block)
Spacer()
} }
.padding() .padding(.vertical, 6)
.background(.bar) .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.down.circle")
}
.disabled(vm.ciStatus == .failure &&
SettingsStore.shared.mergeGateMode == .block)
}
private var prIsOpen: Bool { private var prIsOpen: Bool {
(vm.pr?.state.lowercased() ?? "") == "open" (vm.pr?.state.lowercased() ?? "") == "open"
} }
+36 -20
View File
@@ -142,28 +142,22 @@ struct FilesTab: View {
} }
} }
// MARK: - Commits // MARK: - Overview
/// Renders the PR body as markdown. Uses SwiftUI's built-in
/// `AttributedString(markdown:)` so headings, bold, italic, links and inline
/// code render without a third-party dependency.
struct OverviewTab: View {
let pr: PullRequest
struct CommitsTab: View {
let commits: [PRCommit]
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 12) {
ForEach(commits) { commit in if let body = pr.body, !body.isEmpty {
HStack(alignment: .top, spacing: 10) { MarkdownText(body)
Text(commit.sha ?? commit.id) } else {
.font(.caption2.monospaced()) Text("No description.")
.foregroundColor(.secondary) .font(.callout)
.frame(width: 60, alignment: .leading) .foregroundColor(.secondary)
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) .frame(maxWidth: .infinity, alignment: .leading)
@@ -171,6 +165,28 @@ struct CommitsTab: View {
} }
} }
/// A Text view that renders a markdown string, falling back to plain text if
/// the markdown can't be parsed.
struct MarkdownText: View {
let markdown: String
init(_ markdown: String) {
self.markdown = markdown
}
var body: some View {
if let attributed = try? AttributedString(markdown: markdown) {
Text(attributed)
.font(.callout)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Text(markdown)
.font(.callout)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
// MARK: - Checks // MARK: - Checks
struct ChecksTab: View { struct ChecksTab: View {