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) {