Merge spinner + swipeable PR sections; bump to 3.1
Build iOS App / Build .ipa and publish source (push) Successful in 25s

- MergeSheet: show a spinner in the toolbar while the merge is confirming/
  rejecting, lock Cancel and interactive dismissal during the request, and
  prevent double-taps.
- PR detail: replace the ScrollView-switch with a page-style TabView so you
  can swipe left/right to change sections (iOS 26 style); bottom bar taps
  still select. Make DetailTab Hashable. Bump to 3.1.
This commit is contained in:
Hermes committed 2026-08-13 22:03:14 +00:00
1 parent 2301b8a861
commit ed36ed72de
3 files changed
+36 -16

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 = 21; CURRENT_PROJECT_VERSION = 22;
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 = 3.0; MARKETING_VERSION = 3.1;
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 = 21; CURRENT_PROJECT_VERSION = 22;
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 = 3.0; MARKETING_VERSION = 3.1;
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;
+14 -8
View File
@@ -11,7 +11,7 @@ struct PRDetailView: View {
@State private var requestChangesMode = false @State private var requestChangesMode = false
@State private var showMergeSuccess = false @State private var showMergeSuccess = false
enum DetailTab: String, CaseIterable, Identifiable { enum DetailTab: String, CaseIterable, Identifiable, Hashable {
case overview = "Overview" case overview = "Overview"
case conversation = "Conversation" case conversation = "Conversation"
case files = "Files" case files = "Files"
@@ -103,14 +103,20 @@ struct PRDetailView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
header(pr) header(pr)
ScrollView { // Page-style TabView gives iOS-native swipe-to-change between
switch selectedTab { // sections (matching iOS 26), while the bottom bar taps select.
case .overview: OverviewTab(pr: pr) TabView(selection: $selectedTab) {
case .conversation: ConversationTab(vm: vm) OverviewTab(pr: pr)
case .files: FilesTab(vm: vm) .tag(DetailTab.overview)
case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 }) ConversationTab(vm: vm)
} .tag(DetailTab.conversation)
FilesTab(vm: vm)
.tag(DetailTab.files)
ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 })
.tag(DetailTab.checks)
} }
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(edges: .bottom)
bottomTabBar bottomTabBar
} }
+18 -4
View File
@@ -65,6 +65,7 @@ struct MergeSheet: View {
@ObservedObject var vm: PRDetailViewModel @ObservedObject var vm: PRDetailViewModel
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var method = "merge" @State private var method = "merge"
@State private var isMerging = false
/// Called when the merge succeeds, so the parent can show the success /// Called when the merge succeeds, so the parent can show the success
/// animation and pop back to the landing page. /// animation and pop back to the landing page.
var onSuccess: () -> Void = {} var onSuccess: () -> Void = {}
@@ -118,20 +119,33 @@ struct MergeSheet: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { dismiss() } if isMerging {
// No cancel while a merge is in flight.
} else {
Button("Cancel") { dismiss() }
}
} }
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button("Merge") { performMerge() } if isMerging {
.disabled(SettingsStore.shared.mergeGateMode == .block // Spinner while the merge is confirming/rejecting.
&& vm.ciStatus == .failure) ProgressView()
} else {
Button("Merge") { performMerge() }
.disabled(SettingsStore.shared.mergeGateMode == .block
&& vm.ciStatus == .failure)
}
} }
} }
.interactiveDismissDisabled(isMerging)
} }
} }
private func performMerge() { private func performMerge() {
guard !isMerging else { return }
isMerging = true
Task { Task {
let success = await vm.merge(method: method) let success = await vm.merge(method: method)
isMerging = false
if success { if success {
dismiss() dismiss()
onSuccess() onSuccess()