120 lines
4.0 KiB
Swift
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: " ", with: " ")
|
|
.replacingOccurrences(of: "&", with: "&")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
}
|