diff --git a/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift b/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift index e41a61a..50977a0 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift @@ -12,14 +12,12 @@ struct ArticleView: View { self.entry = entry self.vm = vm _currentEntry = State(initialValue: entry) - // Initialize with whatever content we already have (from slim list or cache) _articleContent = State(initialValue: entry.articleHTML) } var body: some View { Group { if articleContent.isEmpty { - // Only show spinner if we truly have no content at all VStack { Spacer() ProgressView() @@ -34,6 +32,7 @@ struct ArticleView: View { } else { ArticleWebView(html: ArticleHTMLBuilder.build( title: currentEntry.displayTitle, + url: currentEntry.url, feedName: currentEntry.feedName, author: currentEntry.author, timeAgo: currentEntry.timeAgo, @@ -48,71 +47,32 @@ struct ArticleView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { - Button { - Task { await toggleStar() } - } label: { - Image(systemName: currentEntry.starred ? "star.fill" : "star") - .foregroundStyle(currentEntry.starred ? .orange : Color.textTertiary) - } - - Button { - Task { await toggleRead() } - } label: { - Image(systemName: currentEntry.isRead ? "envelope.open" : "envelope.badge") - .foregroundStyle(Color.textTertiary) - } - - Menu { - if currentEntry.url != nil { - Button { - Task { await saveToBrain() } - } label: { - Label( - savedToBrain ? "Saved!" : "Save to Brain", - systemImage: savedToBrain ? "checkmark.circle" : "brain" - ) - } + // Save to Brain + if currentEntry.url != nil { + Button { + Task { await saveToBrain() } + } label: { + Image(systemName: savedToBrain ? "brain.filled.head.profile" : "brain.head.profile") + .foregroundStyle(savedToBrain ? Color.emerald : Color.textTertiary) } - - if currentEntry.fullContent == nil { - Button { - Task { await fetchFull() } - } label: { - Label("Fetch Full Article", systemImage: "arrow.down.doc") - } - } - - if let url = currentEntry.url, let link = URL(string: url) { - ShareLink(item: link) { - Label("Share", systemImage: "square.and.arrow.up") - } - } - } label: { - Image(systemName: "ellipsis.circle") - .foregroundStyle(Color.textTertiary) } } } .task { - // 1. Mark as read — fire-and-forget, don't block UI Task { await vm.markAsRead(entry) } - // 2. Sync toolbar state if let updated = vm.entries.first(where: { $0.id == entry.id }) { currentEntry = updated } - // 3. Fetch full content in background — update when ready do { let fullEntry = try await ReaderAPI().getEntry(id: entry.id) let fullHTML = fullEntry.articleHTML - // Only update if we got better content if !fullHTML.isEmpty && fullHTML.count > articleContent.count { articleContent = fullHTML } - // Preserve local status/starred var merged = fullEntry if let local = vm.entries.first(where: { $0.id == entry.id }) { merged.status = local.status @@ -125,35 +85,6 @@ struct ArticleView: View { } } - private func toggleStar() async { - await vm.toggleStar(currentEntry) - if let updated = vm.entries.first(where: { $0.id == currentEntry.id }) { - currentEntry = updated - } - } - - private func toggleRead() async { - await vm.toggleRead(currentEntry) - if let updated = vm.entries.first(where: { $0.id == currentEntry.id }) { - currentEntry = updated - } - } - - private func fetchFull() async { - isFetchingFull = true - do { - let updated = try await ReaderAPI().fetchFullContent(entryId: currentEntry.id) - articleContent = updated.articleHTML - var merged = updated - if let local = vm.entries.first(where: { $0.id == updated.id }) { - merged.status = local.status - merged.starred = local.starred - } - currentEntry = merged - } catch {} - isFetchingFull = false - } - private func saveToBrain() async { let success = await vm.saveToBrain(currentEntry) if success { @@ -162,10 +93,9 @@ struct ArticleView: View { } } -// MARK: - HTML Builder (stateless, reusable CSS template) +// MARK: - HTML Builder enum ArticleHTMLBuilder { - // Pre-built CSS — avoids rebuilding the same string every article open private static let css = """ * { box-sizing: border-box; } body { @@ -185,6 +115,7 @@ enum ArticleHTMLBuilder { blockquote { border-left-color: #c79e40; color: #9a9590; } td, th { border-color: #333; } .article-meta { color: #8a8580; } + .article-title a { color: #ede8e1 !important; } } .article-header { padding: 16px 0 12px; @@ -195,6 +126,20 @@ enum ArticleHTMLBuilder { font-size: 24px; font-weight: 700; line-height: 1.25; margin: 0 0 8px; } + .article-title a { + color: inherit; + text-decoration: none; + -webkit-tap-highlight-color: rgba(139,105,20,0.15); + } + .article-title a:active { + opacity: 0.6; + } + .external-icon { + font-size: 16px; + opacity: 0.4; + vertical-align: super; + margin-left: 4px; + } .article-meta { font-size: 13px; color: #8B6914; line-height: 1.4; } @@ -213,6 +158,7 @@ enum ArticleHTMLBuilder { static func build( title: String, + url: String?, feedName: String, author: String?, timeAgo: String, @@ -231,6 +177,17 @@ enum ArticleHTMLBuilder { metaParts.append("\(readingTime) read") let meta = metaParts.joined(separator: " · ") + // Title is tappable link to original article (if URL exists) + let titleHTML: String + if let url, !url.isEmpty { + let safeURL = url.replacingOccurrences(of: "\"", with: """) + titleHTML = """ + \(safeTitle) + """ + } else { + titleHTML = safeTitle + } + return """ @@ -241,7 +198,7 @@ enum ArticleHTMLBuilder {
-

\(safeTitle)

+

\(titleHTML)

\(meta)
\(body)