Files
2026-04-28 22:55:10 +03:00

120 lines
4.0 KiB
Swift

import SwiftUI
import SwiftData
import ReadeckCore
import ReadeckNetworking
public struct BookmarkReaderView: View {
@Environment(\.modelContext) private var context
@Environment(AppEnvironment.self) private var appEnvironment
@Bindable var bookmark: Bookmark
@State private var isLoading = false
@State private var errorMessage: String?
public init(bookmark: Bookmark) {
self.bookmark = bookmark
}
public var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(bookmark.title)
.font(.largeTitle.bold())
if let site = bookmark.siteName, !site.isEmpty {
Text(site)
.font(.subheadline)
.foregroundStyle(.secondary)
}
if let html = bookmark.articleHTML, !html.isEmpty {
Text(html.strippedHTML())
.font(.body)
.textSelection(.enabled)
} else if isLoading {
ProgressView("Loading article…")
.frame(maxWidth: .infinity)
.padding(.vertical, 40)
} else {
ContentUnavailableView(
"Article not loaded",
systemImage: "doc.text",
description: Text("Tap refresh to download the article body.")
)
.padding(.vertical, 40)
}
}
.padding()
}
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button {
Task { await toggleMarked() }
} label: {
Image(systemName: bookmark.isMarked ? "star.fill" : "star")
}
Button {
Task { await toggleArchived() }
} label: {
Image(systemName: bookmark.isArchived ? "archivebox.fill" : "archivebox")
}
Button {
Task { await loadArticle(force: true) }
} label: {
Image(systemName: "arrow.clockwise")
}
}
}
.task { await loadArticle(force: false) }
.alert("Action failed", isPresented: .constant(errorMessage != nil), presenting: errorMessage) { _ in
Button("OK") { errorMessage = nil }
} message: { message in
Text(message)
}
}
@MainActor
private func loadArticle(force: Bool) async {
guard !isLoading else { return }
if !force, let html = bookmark.articleHTML, !html.isEmpty { return }
isLoading = true
defer { isLoading = false }
do {
let service = BookmarkSyncService(client: appEnvironment.client, context: context)
try await service.ensureArticleLoaded(for: bookmark, force: force)
} catch {
errorMessage = error.localizedDescription
}
}
@MainActor
private func toggleMarked() async {
do {
let service = BookmarkSyncService(client: appEnvironment.client, context: context)
try await service.toggleMarked(bookmark)
} catch {
errorMessage = error.localizedDescription
}
}
@MainActor
private func toggleArchived() async {
do {
let service = BookmarkSyncService(client: appEnvironment.client, context: context)
try await service.toggleArchived(bookmark)
} catch {
errorMessage = error.localizedDescription
}
}
}
private extension String {
func strippedHTML() -> String {
replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
.replacingOccurrences(of: "&nbsp;", with: " ")
.replacingOccurrences(of: "&amp;", with: "&")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}