diff --git a/URLShare/Base.lproj/MainInterface.storyboard b/URLShare/Base.lproj/MainInterface.storyboard
new file mode 100644
index 0000000..286a508
--- /dev/null
+++ b/URLShare/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/URLShare/Info.plist b/URLShare/Info.plist
new file mode 100644
index 0000000..36d4d3e
--- /dev/null
+++ b/URLShare/Info.plist
@@ -0,0 +1,21 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsWebURLWithMaxCount
+ 1
+
+
+ NSExtensionPointIdentifier
+ com.apple.share-services
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ShareViewController
+
+
+
diff --git a/URLShare/ShareViewController.swift b/URLShare/ShareViewController.swift
new file mode 100644
index 0000000..2a8de35
--- /dev/null
+++ b/URLShare/ShareViewController.swift
@@ -0,0 +1,268 @@
+//
+// ShareViewController.swift
+// URLShare
+//
+// Created by Ilyas Hallak on 11.06.25.
+//
+
+import UIKit
+import Social
+import UniformTypeIdentifiers
+import CoreData
+
+class ShareViewController: SLComposeServiceViewController {
+
+ private var extractedURL: String?
+ private var extractedTitle: String?
+ private var isProcessing = false
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ extractSharedContent()
+ setupUI()
+ }
+
+ override func isContentValid() -> Bool {
+ guard let url = extractedURL,
+ !url.isEmpty,
+ !isProcessing,
+ URL(string: url) != nil else {
+ return false
+ }
+ return true
+ }
+
+ override func didSelectPost() {
+ guard let url = extractedURL else {
+ completeRequest()
+ return
+ }
+
+ isProcessing = true
+ let title = textView.text != extractedTitle ? textView.text : extractedTitle
+
+ // UI Feedback zeigen
+ let loadingAlert = UIAlertController(title: "Speichere Bookmark", message: "Bitte warten...", preferredStyle: .alert)
+ present(loadingAlert, animated: true)
+
+ Task {
+ do {
+ let response = try await createBookmark(url: url, title: title)
+
+ await MainActor.run {
+ loadingAlert.dismiss(animated: true)
+
+ if response.status == 0 {
+ // Erfolg
+ let successAlert = UIAlertController(title: "Erfolg", message: "Bookmark wurde hinzugefügt!", preferredStyle: .alert)
+ successAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
+ self?.completeRequest()
+ })
+ present(successAlert, animated: true)
+ } else {
+ // Fehler vom Server
+ let errorAlert = UIAlertController(title: "Fehler", message: response.message, preferredStyle: .alert)
+ errorAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
+ self?.completeRequest()
+ })
+ present(errorAlert, animated: true)
+ }
+ }
+ } catch {
+ await MainActor.run {
+ loadingAlert.dismiss(animated: true)
+ let errorAlert = UIAlertController(title: "Fehler", message: error.localizedDescription, preferredStyle: .alert)
+ errorAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
+ self?.completeRequest()
+ })
+ present(errorAlert, animated: true)
+ }
+ }
+ }
+ }
+
+ // MARK: - Core Data Token Retrieval
+
+ private func getCurrentToken() -> String? {
+ let context = CoreDataManager.shared.context
+ let request: NSFetchRequest = SettingEntity.fetchRequest() // Anpassen an deine Entity
+
+ do {
+ let tokens = try context.fetch(request)
+ return tokens.first?.token // Anpassen an dein Token-Attribut
+ } catch {
+ print("Failed to fetch token from Core Data: \(error)")
+ return nil
+ }
+ }
+
+ // MARK: - API Implementation
+
+ private func createBookmark(url: String, title: String?) async throws -> CreateBookmarkResponseDto {
+ let requestDto = CreateBookmarkRequestDto(labels: nil, title: title, url: url)
+
+ // Die Server-URL
+ let baseURL = "https://keep.mnk.any64.de"
+ let endpoint = "/api/bookmarks"
+
+ guard let requestURL = URL(string: "\(baseURL)\(endpoint)") else {
+ throw RequestError.invalidURL
+ }
+
+ var request = URLRequest(url: requestURL)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+
+ // Token aus Core Data holen
+ guard let token = getCurrentToken() else {
+ throw RequestError.noToken
+ }
+
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+
+ let encoder = JSONEncoder()
+ request.httpBody = try encoder.encode(requestDto)
+
+ // Request durchführen
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw RequestError.invalidResponse
+ }
+
+ guard 200...299 ~= httpResponse.statusCode else {
+ throw RequestError.serverError(statusCode: httpResponse.statusCode)
+ }
+
+ // Response dekodieren und zurückgeben
+ let decoder = JSONDecoder()
+ return try decoder.decode(CreateBookmarkResponseDto.self, from: data)
+ }
+
+ // MARK: - Support Methods
+
+ private func setupUI() {
+ self.title = "Zu readeck hinzufügen"
+ self.placeholder = "Optional: Titel anpassen..."
+
+ // Zeige URL oder Titel als Kontext
+ if let title = extractedTitle {
+ self.textView.text = title
+ } else if let url = extractedURL {
+ self.textView.text = URL(string: url)?.host ?? url
+ }
+ }
+
+ private func extractSharedContent() {
+ guard let extensionContext = extensionContext else { return }
+
+ for item in extensionContext.inputItems {
+ guard let inputItem = item as? NSExtensionItem else { continue }
+
+ for provider in inputItem.attachments ?? [] {
+ // URL direkt
+ if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
+ provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] item, error in
+ if let url = item as? URL {
+ DispatchQueue.main.async {
+ self?.extractedURL = url.absoluteString
+ self?.extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string
+ self?.setupUI()
+ }
+ }
+ }
+ }
+ // Text (könnte URL enthalten)
+ else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
+ provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] item, error in
+ if let text = item as? String {
+ DispatchQueue.main.async {
+ self?.handleTextContent(text)
+ }
+ }
+ }
+ }
+ // Property List (Safari teilt so)
+ else if provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) {
+ provider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil) { [weak self] item, error in
+ if let dictionary = item as? [String: Any] {
+ DispatchQueue.main.async {
+ self?.handlePropertyList(dictionary)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private func handleTextContent(_ text: String) {
+ let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
+ let range = NSRange(text.startIndex..orderHint
1
+ URLShare.xcscheme_^#shared#^_
+
+ orderHint
+ 1
+
readeck.xcscheme_^#shared#^_
orderHint
diff --git a/readeck/Info.plist b/readeck/Info.plist
new file mode 100644
index 0000000..0f559b5
--- /dev/null
+++ b/readeck/Info.plist
@@ -0,0 +1,17 @@
+
+
+
+
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ readeck.url-handler
+ CFBundleURLSchemes
+
+ readeck
+
+
+
+
+
diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift
index 7993b60..9d21dff 100644
--- a/readeck/UI/Bookmarks/BookmarkCardView.swift
+++ b/readeck/UI/Bookmarks/BookmarkCardView.swift
@@ -73,16 +73,26 @@ struct BookmarkCardView: View {
.multilineTextAlignment(.leading)
}
- // Meta-Info
- HStack {
- if !bookmark.siteName.isEmpty {
- Label(bookmark.siteName, systemImage: "globe")
+ // 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")
+ }
}
- 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)
@@ -106,6 +116,74 @@ struct BookmarkCardView: View {
}
}
+ // 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
diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift
index 58c15fe..d57c276 100644
--- a/readeck/UI/Bookmarks/BookmarksView.swift
+++ b/readeck/UI/Bookmarks/BookmarksView.swift
@@ -3,6 +3,8 @@ import SwiftUI
struct BookmarksView: View {
@State private var viewModel = BookmarksViewModel()
@State private var showingAddBookmark = false
+ @State private var scrollOffset: CGFloat = 0
+ @State private var isScrolling = false
let state: BookmarkState
var body: some View {
@@ -55,16 +57,7 @@ struct BookmarksView: View {
}
}
}
- .navigationTitle(state.displayName)
- .toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button(action: {
- showingAddBookmark = true
- }) {
- Image(systemName: "plus")
- }
- }
- }
+ .navigationTitle(state.displayName)
.sheet(isPresented: $showingAddBookmark) {
AddBookmarkView()
}
@@ -87,5 +80,39 @@ struct BookmarksView: View {
}
}
}
+ .overlay {
+ // Animated FAB Button
+ VStack {
+ Spacer()
+ HStack {
+ Spacer()
+
+ Button(action: {
+ showingAddBookmark = true
+ }) {
+ Image(systemName: "plus")
+ .font(.title2)
+ .fontWeight(.semibold)
+ .foregroundColor(.white)
+ .frame(width: 56, height: 56)
+ .background(Color.accentColor)
+ .clipShape(Circle())
+ .shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3)
+ .scaleEffect(isScrolling ? 0.8 : 1.0)
+ .opacity(isScrolling ? 0.7 : 1.0)
+ }
+ .padding(.trailing, 20)
+ .padding(.bottom, 20)
+ }
+ }
+ }
+ }
+}
+
+// Helper für Scroll-Tracking
+struct ScrollOffsetPreferenceKey: PreferenceKey {
+ static var defaultValue: CGFloat = 0
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+ value = nextValue()
}
}
diff --git a/readeck/Domain/DefaultUseCaseFactory.swift b/readeck/UI/DefaultUseCaseFactory.swift
similarity index 100%
rename from readeck/Domain/DefaultUseCaseFactory.swift
rename to readeck/UI/DefaultUseCaseFactory.swift
diff --git a/readeck/UI/readeckApp.swift b/readeck/UI/readeckApp.swift
index 2aee4a6..6c2dd3e 100644
--- a/readeck/UI/readeckApp.swift
+++ b/readeck/UI/readeckApp.swift
@@ -15,6 +15,35 @@ struct readeckApp: App {
WindowGroup {
MainTabView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
+ .onOpenURL { url in
+ handleIncomingURL(url)
+ }
}
}
+
+ private func handleIncomingURL(_ url: URL) {
+ guard url.scheme == "readeck",
+ url.host == "add-bookmark" else {
+ return
+ }
+
+ let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
+ let queryItems = components?.queryItems
+
+ let urlToAdd = queryItems?.first(where: { $0.name == "url" })?.value
+ let title = queryItems?.first(where: { $0.name == "title" })?.value
+ let notes = queryItems?.first(where: { $0.name == "notes" })?.value
+
+ // Öffne AddBookmarkView mit den Daten
+ // Hier kannst du eine Notification posten oder einen State ändern
+ NotificationCenter.default.post(
+ name: NSNotification.Name("AddBookmarkFromShare"),
+ object: nil,
+ userInfo: [
+ "url": urlToAdd ?? "",
+ "title": title ?? "",
+ "notes": notes ?? ""
+ ]
+ )
+ }
}