From db24c5c4ba994809c7744a68b10ce6d368ed1d29 Mon Sep 17 00:00:00 2001 From: graycreate Date: Sun, 30 Nov 2025 00:17:13 +0800 Subject: [PATCH] feat: Improve RichView image loading, text size, and UI fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix image loading in RichView - Switch from RichView to RichContentView in NewsContentView and ReplyItemView - RichContentView properly renders images using AsyncImageAttachment with Kingfisher 2. Fix text size in topic detail page - Increase default body font from 16 to 17px - Increase compact (reply) font from 14 to 15px - Improve line spacing and paragraph spacing 3. Fix URL escape characters in markdown - Don't escape . and - characters which broke URLs like https://n\.wtf/ - Use raw text for link content instead of escaped text 4. Fix @mention navigation - Add NavigationLink to navigate to UserDetailPage when tapping @username 5. Fix dropdown menu alignment - Position menu directly below navbar instead of overlapping content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Converters/HTMLToMarkdownConverter.swift | 15 +++-- .../RichView/Models/RenderStylesheet.swift | 18 +++--- .../RichView/Views/RichContentView.swift | 10 ++-- V2er/View/Feed/FilterMenuView.swift | 56 +++++++++---------- V2er/View/FeedDetail/NewsContentView.swift | 8 ++- V2er/View/FeedDetail/ReplyItemView.swift | 25 ++++++++- 6 files changed, 79 insertions(+), 53 deletions(-) diff --git a/V2er/Sources/RichView/Converters/HTMLToMarkdownConverter.swift b/V2er/Sources/RichView/Converters/HTMLToMarkdownConverter.swift index e079367..8f4852c 100644 --- a/V2er/Sources/RichView/Converters/HTMLToMarkdownConverter.swift +++ b/V2er/Sources/RichView/Converters/HTMLToMarkdownConverter.swift @@ -88,7 +88,8 @@ public class HTMLToMarkdownConverter { result += "*\(content)*" case "a": - let text = try convertElement(childElement) + // Get raw text without escaping for links + let text = try childElement.text() if let href = try? childElement.attr("href") { result += "[\(text)](\(href))" } else { @@ -213,14 +214,16 @@ public class HTMLToMarkdownConverter { /// Escape special Markdown characters private func escapeMarkdown(_ text: String) -> String { - // Only escape if not already in a code context - // This is a simplified version - a full implementation would track context + // Only escape characters that would cause markdown parsing issues + // Don't escape common characters like . and - as they rarely cause problems + // and escaping them breaks URLs and normal text readability var escaped = text - // Don't escape inside code blocks (this is simplified) + // Don't escape inside code blocks if !text.contains("```") && !text.contains("`") { - // Escape special Markdown characters - let charactersToEscape = ["\\", "*", "_", "[", "]", "(", ")", "#", "+", "-", ".", "!"] + // Only escape the most problematic markdown characters + // Avoid escaping . and - as they appear frequently in URLs and text + let charactersToEscape = ["\\", "*", "_", "[", "]"] for char in charactersToEscape { escaped = escaped.replacingOccurrences(of: char, with: "\\\(char)") } diff --git a/V2er/Sources/RichView/Models/RenderStylesheet.swift b/V2er/Sources/RichView/Models/RenderStylesheet.swift index c839401..eede426 100644 --- a/V2er/Sources/RichView/Models/RenderStylesheet.swift +++ b/V2er/Sources/RichView/Models/RenderStylesheet.swift @@ -50,10 +50,10 @@ public struct TextStyle: Equatable { public var color: Color public init( - fontSize: CGFloat = 16, + fontSize: CGFloat = 17, fontWeight: Font.Weight = .regular, - lineSpacing: CGFloat = 4, - paragraphSpacing: CGFloat = 8, + lineSpacing: CGFloat = 5, + paragraphSpacing: CGFloat = 10, color: Color = .primary ) { self.fontSize = fontSize @@ -264,10 +264,10 @@ extension RenderStylesheet { public static let `default`: RenderStylesheet = { RenderStylesheet( body: TextStyle( - fontSize: 16, + fontSize: 17, fontWeight: .regular, - lineSpacing: 4, - paragraphSpacing: 8, + lineSpacing: 5, + paragraphSpacing: 10, color: .primary ), heading: HeadingStyle( @@ -343,10 +343,10 @@ extension RenderStylesheet { public static let compact: RenderStylesheet = { RenderStylesheet( body: TextStyle( - fontSize: 14, + fontSize: 15, fontWeight: .regular, - lineSpacing: 2, - paragraphSpacing: 6, + lineSpacing: 4, + paragraphSpacing: 8, color: .primary ), heading: HeadingStyle( diff --git a/V2er/Sources/RichView/Views/RichContentView.swift b/V2er/Sources/RichView/Views/RichContentView.swift index 565b6ef..f5433b4 100644 --- a/V2er/Sources/RichView/Views/RichContentView.swift +++ b/V2er/Sources/RichView/Views/RichContentView.swift @@ -49,14 +49,12 @@ public struct RichContentView: View { } else if let error = error { ErrorView(error: error) } else if !contentElements.isEmpty { - ScrollView { - VStack(alignment: .leading, spacing: configuration.stylesheet.body.paragraphSpacing) { - ForEach(contentElements) { element in - renderElement(element) - } + VStack(alignment: .leading, spacing: configuration.stylesheet.body.paragraphSpacing) { + ForEach(contentElements) { element in + renderElement(element) } - .frame(maxWidth: .infinity, alignment: .leading) } + .frame(maxWidth: .infinity, alignment: .leading) } else { Text("No content") .foregroundColor(.secondary) diff --git a/V2er/View/Feed/FilterMenuView.swift b/V2er/View/Feed/FilterMenuView.swift index f64b767..4564f98 100644 --- a/V2er/View/Feed/FilterMenuView.swift +++ b/V2er/View/Feed/FilterMenuView.swift @@ -28,40 +28,40 @@ struct FilterMenuView: View { onDismiss() } - // Menu content - positioned below navbar + // Menu content - positioned right below navbar VStack(spacing: 0) { - HStack { - Spacer() - ScrollView { - VStack(spacing: 4) { - ForEach(Tab.allTabs, id: \.self) { tab in - let tabNeedsLogin = tab.needsLogin() && !AccountState.hasSignIn() - TabFilterMenuItem( - tab: tab, - isSelected: tab == selectedTab, - needsLogin: tabNeedsLogin - ) { - // Soft haptic feedback - let impactFeedback = UIImpactFeedbackGenerator(style: .light) - impactFeedback.impactOccurred() - - if tabNeedsLogin { - Toast.show("登录后才能查看「\(tab.displayName())」下的内容") - } else { - onTabSelected(tab) - } + // Minimal spacing - just clear the safe area + Spacer() + .frame(height: topSafeAreaInset().top) + + ScrollView { + VStack(spacing: 4) { + ForEach(Tab.allTabs, id: \.self) { tab in + let tabNeedsLogin = tab.needsLogin() && !AccountState.hasSignIn() + TabFilterMenuItem( + tab: tab, + isSelected: tab == selectedTab, + needsLogin: tabNeedsLogin + ) { + // Soft haptic feedback + let impactFeedback = UIImpactFeedbackGenerator(style: .light) + impactFeedback.impactOccurred() + + if tabNeedsLogin { + Toast.show("登录后才能查看「\(tab.displayName())」下的内容") + } else { + onTabSelected(tab) } } } - .padding(.vertical, 8) } - .frame(width: 200) - .background(Color.itemBg) - .cornerRadius(8) - .shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4) - .frame(maxHeight: 450) - Spacer() + .padding(.vertical, 8) } + .frame(width: 200) + .background(Color.itemBg) + .cornerRadius(8) + .shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4) + .frame(maxHeight: 450) .transition(.asymmetric( insertion: .move(edge: .top).combined(with: .opacity), removal: .move(edge: .top).combined(with: .opacity) diff --git a/V2er/View/FeedDetail/NewsContentView.swift b/V2er/View/FeedDetail/NewsContentView.swift index ef08e70..ec166c3 100644 --- a/V2er/View/FeedDetail/NewsContentView.swift +++ b/V2er/View/FeedDetail/NewsContentView.swift @@ -25,11 +25,15 @@ struct NewsContentView: View { VStack(spacing: 0) { Divider() - RichView(htmlContent: contentInfo?.html ?? "") + RichContentView(htmlContent: contentInfo?.html ?? "") .configuration(configurationForAppearance()) .onLinkTapped { url in handleLinkTap(url) } + .onImageTapped { url in + // Open image in SafariView for now + openInSafari(url) + } .onRenderCompleted { metadata in // Mark as rendered after content is ready DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -40,6 +44,8 @@ struct NewsContentView: View { print("Render error: \(error)") self.rendered = true } + .padding(.horizontal, 12) + .padding(.vertical, 8) Divider() } diff --git a/V2er/View/FeedDetail/ReplyItemView.swift b/V2er/View/FeedDetail/ReplyItemView.swift index 64122f5..1799e74 100644 --- a/V2er/View/FeedDetail/ReplyItemView.swift +++ b/V2er/View/FeedDetail/ReplyItemView.swift @@ -16,6 +16,7 @@ struct ReplyItemView: View { @Environment(\.colorScheme) var colorScheme @State private var showingSafari = false @State private var safariURL: URL? + @State private var navigateToUser: String? = nil var body: some View { HStack(alignment: .top) { @@ -50,14 +51,16 @@ struct ReplyItemView: View { .foregroundColor(info.hadThanked ? .red : .secondaryText) } - RichView(htmlContent: info.content) + RichContentView(htmlContent: info.content) .configuration(compactConfigurationForAppearance()) .onLinkTapped { url in handleLinkTap(url) } + .onImageTapped { url in + openInSafari(url) + } .onMentionTapped { username in - print("Navigate to mentioned user: @\(username)") - // TODO: Implement proper navigation to UserDetailPage + navigateToUser = username } Text("\(info.floor)楼") @@ -73,6 +76,22 @@ struct ReplyItemView: View { SafariView(url: url) } } + .background( + NavigationLink( + destination: Group { + if let username = navigateToUser { + UserDetailPage(userId: username) + } + }, + isActive: Binding( + get: { navigateToUser != nil }, + set: { if !$0 { navigateToUser = nil } } + ) + ) { + EmptyView() + } + .hidden() + ) } private func handleLinkTap(_ url: URL) {