From dd0fc84238805f42d3a97bacbbc0cbc0b4e95e60 Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 14 Aug 2026 09:43:39 +0000 Subject: [PATCH] Restore vertical scroll + line-based markdown; swipe on bottom bar; bump to 3.3 The horizontal paging ScrollView squished the markdown text and the TabView broke navigation. Revert to the plain vertical ScrollView + switch (always rendered correctly). Swipe-to-change now lives on the bottom tab bar itself (simultaneous DragGesture), so tapping tabs still works. Rewrite MarkdownText line-based: each source line renders on its own line (AttributedString collapses single newlines into spaces), with heading/bullet/code handling. Bump to 3.3. --- GiteaPR.xcodeproj/project.pbxproj | 8 ++--- GiteaPR/Detail/PRDetailView.swift | 48 ++++++++++++++-------------- GiteaPR/Detail/Tabs.swift | 52 +++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/GiteaPR.xcodeproj/project.pbxproj b/GiteaPR.xcodeproj/project.pbxproj index 1784e0a..88354ff 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 = 23; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -197,7 +197,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.2; + MARKETING_VERSION = 3.3; 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 = 23; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR"; @@ -221,7 +221,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.2; + MARKETING_VERSION = 3.3; 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 0958846..b69067f 100644 --- a/GiteaPR/Detail/PRDetailView.swift +++ b/GiteaPR/Detail/PRDetailView.swift @@ -103,32 +103,17 @@ struct PRDetailView: View { VStack(spacing: 0) { header(pr) - // Horizontal paging ScrollView — swipeable like a TabView, but - // without breaking NavigationLink inside the tabs (TabView's .page - // style swallows taps and interferes with nested navigation). - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 0) { - OverviewTab(pr: pr) - .id(DetailTab.overview) - .containerRelativeFrame(.horizontal) - - ConversationTab(vm: vm) - .id(DetailTab.conversation) - .containerRelativeFrame(.horizontal) - - FilesTab(vm: vm) - .id(DetailTab.files) - .containerRelativeFrame(.horizontal) - - ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 }) - .id(DetailTab.checks) - .containerRelativeFrame(.horizontal) + // Vertical scroll — has always rendered correctly (a horizontal + // ScrollView/TabView broke NavigationLink and mangled markdown). + 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 }) + case .none: OverviewTab(pr: pr) } - .scrollTargetLayout() } - .scrollTargetBehavior(.paging) - .scrollPosition(id: $selectedTab) - .ignoresSafeArea(edges: .bottom) bottomTabBar } @@ -204,6 +189,21 @@ struct PRDetailView: View { ) .padding(.horizontal, 20) .padding(.bottom, 8) + // Swipe across the bar to change sections (iOS 26 feel). Taps on the + // individual tab buttons still work — simultaneous gesture. + .simultaneousGesture( + DragGesture(minimumDistance: 20) + .onEnded { value in + let tabs = DetailTab.allCases + guard let current = selectedTab ?? .overview, + let idx = tabs.firstIndex(of: current) else { return } + if value.translation.width < -40, idx < tabs.count - 1 { + withAnimation { selectedTab = tabs[idx + 1] } + } else if value.translation.width > 40, idx > 0 { + withAnimation { selectedTab = tabs[idx - 1] } + } + } + ) } private var reviewMenu: some View { diff --git a/GiteaPR/Detail/Tabs.swift b/GiteaPR/Detail/Tabs.swift index f2b2922..f665dce 100644 --- a/GiteaPR/Detail/Tabs.swift +++ b/GiteaPR/Detail/Tabs.swift @@ -167,6 +167,11 @@ struct OverviewTab: View { /// A Text view that renders a markdown string, falling back to plain text if /// the markdown can't be parsed. +/// +/// Line-based: each source line renders on its own line (AttributedString +/// collapses single newlines into spaces, which squishes PR bodies that use +/// one-line-per-key formatting). Handles headings, bullets, and code fences; +/// inline formatting (bold, italic, links, inline code) via AttributedString. struct MarkdownText: View { let markdown: String @@ -175,12 +180,55 @@ struct MarkdownText: View { } var body: some View { - if let attributed = try? AttributedString(markdown: markdown) { + VStack(alignment: .leading, spacing: 6) { + let lines = markdown.components(separatedBy: "\n") + var inCodeBlock = false + ForEach(Array(lines.enumerated()), id: \.offset) { _, line in + if line.hasPrefix("```") { + inCodeBlock.toggle() + } else if inCodeBlock { + Text(line) + .font(.caption.monospaced()) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(4) + .background(Color(.secondarySystemBackground)) + } else if line.hasPrefix("# ") || line.hasPrefix("## ") || line.hasPrefix("### ") { + heading(line) + } else if line.hasPrefix("- ") || line.hasPrefix("* ") { + bullet(line) + } else if line.trimmingCharacters(in: .whitespaces).isEmpty { + Spacer(minLength: 4) + } else { + inline(line) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func heading(_ line: String) -> some View { + let level = line.prefix(while: { $0 == "#" }).count + let text = line.dropFirst(level).trimmingCharacters(in: .whitespaces) + return Text(text) + .font(level == 1 ? .title2.bold() : level == 2 ? .title3.bold() : .headline) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func bullet(_ line: String) -> some View { + HStack(alignment: .top, spacing: 6) { + Text("•") + inline(String(line.dropFirst(2))) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func inline(_ text: String) -> some View { + if let attributed = try? AttributedString(markdown: text) { Text(attributed) .font(.callout) .frame(maxWidth: .infinity, alignment: .leading) } else { - Text(markdown) + Text(text) .font(.callout) .frame(maxWidth: .infinity, alignment: .leading) }