diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 32044a8..ac8ec19 100644 --- a/GiteaPR.xcodeproj/project.pbxproj +++ b/GiteaPR.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 22; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; + MARKETING_VERSION = 3.1; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -211,7 +211,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 22; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0; + MARKETING_VERSION = 3.1; PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/GiteaPR/Detail/PRDetailView.swift b/GiteaPR/Detail/PRDetailView.swift index 0338747..3198e5e 100644 --- a/GiteaPR/Detail/PRDetailView.swift +++ b/GiteaPR/Detail/PRDetailView.swift @@ -11,7 +11,7 @@ struct PRDetailView: View { @State private var requestChangesMode = false @State private var showMergeSuccess = false - enum DetailTab: String, CaseIterable, Identifiable { + enum DetailTab: String, CaseIterable, Identifiable, Hashable { case overview = "Overview" case conversation = "Conversation" case files = "Files" @@ -103,14 +103,20 @@ struct PRDetailView: View { VStack(spacing: 0) { header(pr) - ScrollView { - switch selectedTab { - case .overview: OverviewTab(pr: pr) - case .conversation: ConversationTab(vm: vm) - case .files: FilesTab(vm: vm) - case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 }) - } + // Page-style TabView gives iOS-native swipe-to-change between + // sections (matching iOS 26), while the bottom bar taps select. + TabView(selection: $selectedTab) { + OverviewTab(pr: pr) + .tag(DetailTab.overview) + 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 } diff --git a/GiteaPR/Detail/Sheets.swift b/GiteaPR/Detail/Sheets.swift index 83ed7e4..81f4e33 100644 --- a/GiteaPR/Detail/Sheets.swift +++ b/GiteaPR/Detail/Sheets.swift @@ -65,6 +65,7 @@ struct MergeSheet: View { @ObservedObject var vm: PRDetailViewModel @Environment(\.dismiss) private var dismiss @State private var method = "merge" + @State private var isMerging = false /// Called when the merge succeeds, so the parent can show the success /// animation and pop back to the landing page. var onSuccess: () -> Void = {} @@ -118,20 +119,33 @@ struct MergeSheet: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } + if isMerging { + // No cancel while a merge is in flight. + } else { + Button("Cancel") { dismiss() } + } } ToolbarItem(placement: .confirmationAction) { - Button("Merge") { performMerge() } - .disabled(SettingsStore.shared.mergeGateMode == .block - && vm.ciStatus == .failure) + if isMerging { + // Spinner while the merge is confirming/rejecting. + ProgressView() + } else { + Button("Merge") { performMerge() } + .disabled(SettingsStore.shared.mergeGateMode == .block + && vm.ciStatus == .failure) + } } } + .interactiveDismissDisabled(isMerging) } } private func performMerge() { + guard !isMerging else { return } + isMerging = true Task { let success = await vm.merge(method: method) + isMerging = false if success { dismiss() onSuccess()