From 8882a402ef8eaebdee8f8181c30b8e1f0849f23c Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Fri, 13 Jun 2025 15:02:46 +0200 Subject: [PATCH] refactor: Simplify Share Extension to open main app directly - Refactor ShareViewController to extract URL and open main app instead of direct API calls - Add robust URL extraction from multiple content types (URL, text, property lists) - Implement comprehensive debugging for Share Extension content processing - Add URL scheme handling in main app (readeck://add-bookmark) - Add notification-based communication between Share Extension and main app - Extend BookmarksViewModel with share notification handling - Support automatic AddBookmarkView opening with prefilled URL and title from shares Technical changes: - Remove Core Data dependency from Share Extension - Add extensionContext.open() for launching main app with custom URL scheme - Process all registered type identifiers for robust content extraction - Add NSDataDetector for URL extraction from plain text - Handle Safari property list sharing format - Add share state management in BookmarksViewModel (@Observable pattern) - Implement NotificationCenter publisher pattern with Combine URL scheme format: readeck://add-bookmark?url=...&title=... Notification: 'AddBookmarkFromShare' with url and title in userInfo --- URLShare/ShareViewController.swift | 367 ++++++++---------- readeck/UI/AddBookmark/AddBookmarkView.swift | 12 +- .../UI/AddBookmark/AddBookmarkViewModel.swift | 11 +- readeck/UI/Bookmarks/BookmarksView.swift | 23 +- readeck/UI/Bookmarks/BookmarksViewModel.swift | 35 +- readeck/UI/TabView.swift | 2 +- 6 files changed, 236 insertions(+), 214 deletions(-) diff --git a/URLShare/ShareViewController.swift b/URLShare/ShareViewController.swift index 2a8de35..cd418e8 100644 --- a/URLShare/ShareViewController.swift +++ b/URLShare/ShareViewController.swift @@ -8,188 +8,59 @@ 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() + + // Automatisch die Haupt-App öffnen, sobald URL extrahiert wurde + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.openParentApp() + } } override func isContentValid() -> Bool { - guard let url = extractedURL, - !url.isEmpty, - !isProcessing, - URL(string: url) != nil else { - return false - } - return true + return true // Immer true, damit der Button funktioniert } 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) - } - } - } + openMainApp() } - // 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 - } - } + // MARK: - Private Methods private func extractSharedContent() { guard let extensionContext = extensionContext else { return } - for item in extensionContext.inputItems { + print("=== DEBUG: Starting content extraction ===") + print("Input items count: \(extensionContext.inputItems.count)") + + for (itemIndex, item) in extensionContext.inputItems.enumerated() { 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() - } + print("Item \(itemIndex) - attachments: \(inputItem.attachments?.count ?? 0)") + + // Versuche alle verfügbaren Type Identifiers + for (attachmentIndex, provider) in (inputItem.attachments ?? []).enumerated() { + print("Attachment \(attachmentIndex) - registered types: \(provider.registeredTypeIdentifiers)") + + // Iteriere durch alle registrierten Type Identifiers + for typeIdentifier in provider.registeredTypeIdentifiers { + print("Trying type identifier: \(typeIdentifier)") + + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { [weak self] item, error in + if let error = error { + print("Error loading \(typeIdentifier): \(error)") + return } - } - } - // 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) - } + + DispatchQueue.main.async { + self?.processLoadedItem(item, typeIdentifier: typeIdentifier, inputItem: inputItem) } } } @@ -197,72 +68,156 @@ class ShareViewController: SLComposeServiceViewController { } } - private func handleTextContent(_ text: String) { + private func processLoadedItem(_ item: Any?, typeIdentifier: String, inputItem: NSExtensionItem) { + print("Processing item of type \(typeIdentifier): \(type(of: item))") + + // URL direkt + if let url = item as? URL { + print("Found URL: \(url.absoluteString)") + extractedURL = url.absoluteString + extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string + return + } + + // NSURL + if let nsurl = item as? NSURL { + print("Found NSURL: \(nsurl.absoluteString ?? "nil")") + extractedURL = nsurl.absoluteString + extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string + return + } + + // String (könnte URL sein) + if let text = item as? String { + print("Found String: \(text)") + if URL(string: text) != nil { + extractedURL = text + extractedTitle = inputItem.attributedTitle?.string ?? inputItem.attributedContentText?.string + return + } + + // Versuche URL aus Text zu extrahieren + if let extractedURL = extractURLFromText(text) { + self.extractedURL = extractedURL + self.extractedTitle = text != extractedURL ? text : nil + return + } + } + + // Dictionary (Property List) + if let dictionary = item as? [String: Any] { + print("Found Dictionary: \(dictionary)") + handlePropertyList(dictionary) + return + } + + // NSData - versuche als String zu interpretieren + if let data = item as? Data { + if let text = String(data: data, encoding: .utf8) { + print("Found Data as String: \(text)") + if URL(string: text) != nil { + extractedURL = text + return + } + } + } + + print("Could not process item of type: \(type(of: item))") + } + + private func extractURLFromText(_ text: String) -> String? { let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) let range = NSRange(text.startIndex..() init() { - + setupNotificationObserver() + } + + private func setupNotificationObserver() { + NotificationCenter.default + .publisher(for: NSNotification.Name("AddBookmarkFromShare")) + .sink { [weak self] notification in + self?.handleShareNotification(notification) + } + .store(in: &cancellables) + } + + private func handleShareNotification(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let url = userInfo["url"] as? String, + !url.isEmpty else { + return + } + + DispatchQueue.main.async { + self.shareURL = url + self.shareTitle = userInfo["title"] as? String ?? "" + self.showingAddBookmarkFromShare = true + } + + print("Received share notification - URL: \(url)") } @MainActor diff --git a/readeck/UI/TabView.swift b/readeck/UI/TabView.swift index 3a77c46..b8cc0bd 100644 --- a/readeck/UI/TabView.swift +++ b/readeck/UI/TabView.swift @@ -1,7 +1,7 @@ import SwiftUI import Foundation -enum BookmarkState: String, CaseIterable { +enum BookmarkState: String, CaseIterable { case unread = "unread" case favorite = "favorite" case archived = "archived"