- Add BookmarkState enum with unread, favorite, and archived states - Extend API layer with query parameter filtering for bookmark states - Update Bookmark domain model to match complete API response schema - Implement BookmarkListView with card-based UI and preview images - Add BookmarkListViewModel with state management and error handling - Enhance BookmarkDetailView with meta information and WebView rendering - Create comprehensive DTO mapping for all bookmark fields - Add TabView with state-based bookmark filtering - Implement date formatting utilities for ISO8601 timestamps - Add progress indicators and pull-to-refresh functionality
120 lines
4.3 KiB
Swift
120 lines
4.3 KiB
Swift
import SwiftUI
|
|
|
|
struct BookmarkDetailView: View {
|
|
let bookmarkId: String
|
|
@State private var viewModel = BookmarkDetailViewModel()
|
|
@State private var webViewHeight: CGFloat = 300
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
// Header mit Bild
|
|
if !viewModel.bookmarkDetail.imageUrl.isEmpty {
|
|
AsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) { image in
|
|
image
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
} placeholder: {
|
|
Rectangle()
|
|
.fill(Color.gray.opacity(0.3))
|
|
.frame(height: 200)
|
|
}
|
|
.frame(height: 200)
|
|
.clipped()
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
// Titel
|
|
Text(viewModel.bookmarkDetail.title)
|
|
.font(.largeTitle)
|
|
.fontWeight(.bold)
|
|
|
|
// Meta-Informationen
|
|
metaInfoSection
|
|
|
|
Divider()
|
|
|
|
// Artikel-Inhalt mit WebView
|
|
if !viewModel.articleContent.isEmpty {
|
|
WebView(htmlContent: viewModel.articleContent) { height in
|
|
webViewHeight = height
|
|
}
|
|
.frame(height: webViewHeight)
|
|
} else if viewModel.isLoadingArticle {
|
|
ProgressView("Lade Artikel...")
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding()
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task {
|
|
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
|
await viewModel.loadArticleContent(id: bookmarkId)
|
|
}
|
|
}
|
|
|
|
private var metaInfoSection: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if !viewModel.bookmarkDetail.authors.isEmpty {
|
|
HStack {
|
|
Image(systemName: "person")
|
|
Text(viewModel.bookmarkDetail.authors.joined(separator: ", "))
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Image(systemName: "calendar")
|
|
Text("Erstellt: \(formatDate(viewModel.bookmarkDetail.created))")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
HStack {
|
|
Image(systemName: "textformat")
|
|
Text("\(viewModel.bookmarkDetail.wordCount) Wörter • \(viewModel.bookmarkDetail.readingTime) min Lesezeit")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func formatDate(_ dateString: String) -> String {
|
|
// Erstelle einen Formatter für das ISO8601-Format mit Millisekunden
|
|
let isoFormatter = ISO8601DateFormatter()
|
|
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
|
|
// Fallback für Format ohne Millisekunden
|
|
let isoFormatterNoMillis = ISO8601DateFormatter()
|
|
isoFormatterNoMillis.formatOptions = [.withInternetDateTime]
|
|
|
|
// Versuche beide Formate
|
|
var date: Date?
|
|
if let parsedDate = isoFormatter.date(from: dateString) {
|
|
date = parsedDate
|
|
} else if let parsedDate = isoFormatterNoMillis.date(from: dateString) {
|
|
date = parsedDate
|
|
}
|
|
|
|
if let date = date {
|
|
let displayFormatter = DateFormatter()
|
|
displayFormatter.dateStyle = .medium
|
|
displayFormatter.timeStyle = .short
|
|
displayFormatter.locale = Locale(identifier: "de_DE")
|
|
return displayFormatter.string(from: date)
|
|
}
|
|
|
|
return dateString
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationView {
|
|
BookmarkDetailView(bookmarkId: "sample-id")
|
|
}
|
|
}
|