ReadKeep/readeck/UI/Bookmarks/BookmarkCardView.swift
Ilyas Hallak 82f9d8a5a9 feat: Add URL Share Extension with API integration
- Add ShareViewController with complete URL extraction logic
- Support URL sharing from Safari, Chrome and other apps
- Extract URLs from different content types (URL, plain text, property list)
- Implement direct API call to create bookmarks from share extension
- Add Core Data integration to fetch authentication token
- Include proper error handling and user feedback with alerts
- Support title extraction and user text input for bookmark creation

Technical implementation:
- Handle UTType.url, UTType.plainText, and UTType.propertyList
- Async/await pattern for API requests
- NSDataDetector for URL extraction from text
- Property list parsing for Safari-style sharing
- Loading and success/error alerts for better UX
2025-06-12 23:21:55 +02:00

241 lines
8.2 KiB
Swift

import SwiftUI
struct BookmarkCardView: View {
let bookmark: Bookmark
let currentState: BookmarkState
let onArchive: (Bookmark) -> Void
let onDelete: (Bookmark) -> Void
let onToggleFavorite: (Bookmark) -> Void
@State private var showingActionSheet = false
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Vorschaubild - verwende image oder thumbnail
AsyncImage(url: imageURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.2))
.overlay {
Image(systemName: "photo")
.foregroundColor(.gray)
}
}
.frame(height: 120)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 4) {
// Status-Icons und Action-Button
HStack {
HStack(spacing: 6) {
if bookmark.isMarked {
IconBadge(systemName: "heart.fill", color: .red)
}
if bookmark.isArchived {
IconBadge(systemName: "archivebox.fill", color: .gray)
}
if bookmark.hasArticle {
IconBadge(systemName: "doc.text.fill", color: .green)
}
}
Spacer()
// Action Menu Button
Button(action: {
showingActionSheet = true
}) {
Image(systemName: "ellipsis")
.foregroundColor(.secondary)
.padding(8)
.background(Color.gray.opacity(0.1))
.clipShape(Circle())
}
.buttonStyle(PlainButtonStyle())
}
// Titel
Text(bookmark.title)
.font(.headline)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.leading)
// Beschreibung
if !bookmark.description.isEmpty {
Text(bookmark.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(3)
.multilineTextAlignment(.leading)
}
// Meta-Info mit Datum
VStack(alignment: .leading, spacing: 4) {
HStack {
if !bookmark.siteName.isEmpty {
Label(bookmark.siteName, systemImage: "globe")
}
Spacer()
if let readingTime = bookmark.readingTime, readingTime > 0 {
Label("\(readingTime) min", systemImage: "clock")
}
}
// Veröffentlichungsdatum
if let publishedDate = formattedPublishedDate {
HStack {
Label(publishedDate, systemImage: "calendar")
Spacer()
}
}
}
.font(.caption)
.foregroundColor(.secondary)
// Progress Bar für Lesefortschritt
if bookmark.readProgress > 0 {
ProgressView(value: Double(bookmark.readProgress), total: 100)
.progressViewStyle(LinearProgressViewStyle())
.frame(height: 4)
}
}
.padding(.horizontal, 12)
.padding(.bottom, 12)
}
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
.confirmationDialog("Bookmark Aktionen", isPresented: $showingActionSheet) {
actionButtons
}
}
// MARK: - Computed Properties
private var formattedPublishedDate: String? {
guard let published = bookmark.published,
!published.isEmpty else {
return nil
}
// Prüfe auf Unix Epoch (1970-01-01) - bedeutet "kein Datum"
if published.contains("1970-01-01") {
return nil
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
guard let date = formatter.date(from: published) else {
// Fallback ohne Millisekunden
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
guard let fallbackDate = formatter.date(from: published) else {
return nil
}
return formatDate(fallbackDate)
}
return formatDate(date)
}
private func formatDate(_ date: Date) -> String {
let now = Date()
let calendar = Calendar.current
// Heute
if calendar.isDateInToday(date) {
let formatter = DateFormatter()
formatter.timeStyle = .short
return "Heute, \(formatter.string(from: date))"
}
// Gestern
if calendar.isDateInYesterday(date) {
let formatter = DateFormatter()
formatter.timeStyle = .short
return "Gestern, \(formatter.string(from: date))"
}
// Diese Woche
if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, HH:mm"
return formatter.string(from: date)
}
// Dieses Jahr
if calendar.isDate(date, equalTo: now, toGranularity: .year) {
let formatter = DateFormatter()
formatter.dateFormat = "d. MMM, HH:mm"
return formatter.string(from: date)
}
// Andere Jahre
let formatter = DateFormatter()
formatter.dateFormat = "d. MMM yyyy"
return formatter.string(from: date)
}
private var actionButtons: some View {
Group {
// Favorit Toggle
Button(bookmark.isMarked ? "Favorit entfernen" : "Als Favorit markieren") {
onToggleFavorite(bookmark)
}
// Archivieren/Dearchivieren basierend auf aktuellem State
if currentState == .archived {
Button("Aus Archiv entfernen") {
onArchive(bookmark)
}
} else {
Button("Archivieren") {
onArchive(bookmark)
}
}
// Permanent löschen (immer verfügbar)
Button("Permanent löschen", role: .destructive) {
onDelete(bookmark)
}
Button("Abbrechen", role: .cancel) { }
}
}
private var imageURL: URL? {
// Bevorzuge image, dann thumbnail, dann icon
if let imageUrl = bookmark.resources.image?.src {
return URL(string: imageUrl)
} else if let thumbnailUrl = bookmark.resources.thumbnail?.src {
return URL(string: thumbnailUrl)
} else if let iconUrl = bookmark.resources.icon?.src {
return URL(string: iconUrl)
}
return nil
}
}
struct IconBadge: View {
let systemName: String
let color: Color
var body: some View {
Image(systemName: systemName)
.font(.caption2)
.padding(6)
.background(color.opacity(0.2))
.foregroundColor(color)
.clipShape(Circle())
}
}