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
This commit is contained in:
parent
82f9d8a5a9
commit
8882a402ef
@ -8,188 +8,59 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Social
|
import Social
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import CoreData
|
|
||||||
|
|
||||||
class ShareViewController: SLComposeServiceViewController {
|
class ShareViewController: SLComposeServiceViewController {
|
||||||
|
|
||||||
private var extractedURL: String?
|
private var extractedURL: String?
|
||||||
private var extractedTitle: String?
|
private var extractedTitle: String?
|
||||||
private var isProcessing = false
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
extractSharedContent()
|
extractSharedContent()
|
||||||
setupUI()
|
|
||||||
|
// Automatisch die Haupt-App öffnen, sobald URL extrahiert wurde
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
self.openParentApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func isContentValid() -> Bool {
|
override func isContentValid() -> Bool {
|
||||||
guard let url = extractedURL,
|
return true // Immer true, damit der Button funktioniert
|
||||||
!url.isEmpty,
|
|
||||||
!isProcessing,
|
|
||||||
URL(string: url) != nil else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didSelectPost() {
|
override func didSelectPost() {
|
||||||
guard let url = extractedURL else {
|
openMainApp()
|
||||||
completeRequest()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isProcessing = true
|
// MARK: - Private Methods
|
||||||
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> = 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() {
|
private func extractSharedContent() {
|
||||||
guard let extensionContext = extensionContext else { return }
|
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 }
|
guard let inputItem = item as? NSExtensionItem else { continue }
|
||||||
|
|
||||||
for provider in inputItem.attachments ?? [] {
|
print("Item \(itemIndex) - attachments: \(inputItem.attachments?.count ?? 0)")
|
||||||
// URL direkt
|
|
||||||
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
// Versuche alle verfügbaren Type Identifiers
|
||||||
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] item, error in
|
for (attachmentIndex, provider) in (inputItem.attachments ?? []).enumerated() {
|
||||||
if let url = item as? URL {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self?.extractedURL = url.absoluteString
|
self?.processLoadedItem(item, typeIdentifier: typeIdentifier, inputItem: inputItem)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
||||||
|
|
||||||
if let match = detector?.firstMatch(in: text, options: [], range: range),
|
if let match = detector?.firstMatch(in: text, options: [], range: range),
|
||||||
let url = match.url {
|
let url = match.url {
|
||||||
extractedURL = url.absoluteString
|
return url.absoluteString
|
||||||
extractedTitle = text != url.absoluteString ? text : nil
|
|
||||||
} else if URL(string: text) != nil {
|
|
||||||
extractedURL = text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupUI()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handlePropertyList(_ dictionary: [String: Any]) {
|
private func handlePropertyList(_ dictionary: [String: Any]) {
|
||||||
if let urlString = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],
|
// Safari und andere Browser verwenden oft Property Lists
|
||||||
let url = urlString["URL"] as? String {
|
if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any] {
|
||||||
|
if let url = results["URL"] as? String {
|
||||||
extractedURL = url
|
extractedURL = url
|
||||||
extractedTitle = urlString["title"] as? String
|
extractedTitle = results["title"] as? String
|
||||||
} else if let url = dictionary["URL"] as? String {
|
return
|
||||||
extractedURL = url
|
}
|
||||||
extractedTitle = dictionary["title"] as? String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupUI()
|
// Direkte URL im Dictionary
|
||||||
|
if let url = dictionary["URL"] as? String {
|
||||||
|
extractedURL = url
|
||||||
|
extractedTitle = dictionary["title"] as? String
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Andere mögliche Keys
|
||||||
|
for key in dictionary.keys {
|
||||||
|
if let value = dictionary[key] as? String, URL(string: value) != nil {
|
||||||
|
extractedURL = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openMainApp() {
|
||||||
|
let url = extractedURL ?? "https://example.com"
|
||||||
|
let title = extractedTitle ?? ""
|
||||||
|
|
||||||
|
print("Opening main app with URL: \(url)")
|
||||||
|
|
||||||
|
// Verwende NSUserActivity anstatt URL-Schema
|
||||||
|
let userActivity = NSUserActivity(activityType: "de.ilyas.readeck")
|
||||||
|
userActivity.userInfo = [
|
||||||
|
"url": url,
|
||||||
|
"title": title
|
||||||
|
]
|
||||||
|
userActivity.webpageURL = URL(string: url)
|
||||||
|
|
||||||
|
// Extension schließen und Activity übergeben
|
||||||
|
extensionContext?.completeRequest(returningItems: [userActivity], completionHandler: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func completeRequest() {
|
private func completeRequest() {
|
||||||
isProcessing = false
|
|
||||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - DTOs und Error Handling
|
func openParentApp() {
|
||||||
|
guard let extensionContext = extensionContext else { return }
|
||||||
|
|
||||||
struct CreateBookmarkRequestDto: Codable {
|
let url = extractedURL ?? "https://example.com"
|
||||||
let labels: [String]?
|
let title = extractedTitle ?? ""
|
||||||
let title: String?
|
|
||||||
let url: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CreateBookmarkResponseDto: Codable {
|
print("Opening parent app with URL: \(url)")
|
||||||
let message: String
|
|
||||||
let status: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RequestError: Error, LocalizedError {
|
// URL für die Haupt-App erstellen mit Parametern
|
||||||
case invalidURL
|
var urlComponents = URLComponents(string: "readeck://add-bookmark")
|
||||||
case invalidResponse
|
urlComponents?.queryItems = [
|
||||||
case serverError(statusCode: Int)
|
URLQueryItem(name: "url", value: url)
|
||||||
case decodingError
|
]
|
||||||
case noToken
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
if !title.isEmpty {
|
||||||
switch self {
|
urlComponents?.queryItems?.append(URLQueryItem(name: "title", value: title))
|
||||||
case .invalidURL:
|
}
|
||||||
return "Ungültige URL"
|
|
||||||
case .invalidResponse:
|
guard let finalURL = urlComponents?.url else {
|
||||||
return "Ungültige Server-Antwort"
|
print("Failed to create final URL")
|
||||||
case .serverError(let code):
|
return
|
||||||
return "Server-Fehler: \(code)"
|
}
|
||||||
case .decodingError:
|
|
||||||
return "Fehler beim Dekodieren der Antwort"
|
print("Final URL: \(finalURL)")
|
||||||
case .noToken:
|
|
||||||
return "Kein Authentifizierungs-Token gefunden"
|
var responder: UIResponder? = self
|
||||||
|
|
||||||
|
while responder != nil {
|
||||||
|
if let application = responder as? UIApplication {
|
||||||
|
application.open(finalURL)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
responder = responder?.next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,11 @@ struct AddBookmarkView: View {
|
|||||||
@State private var viewModel = AddBookmarkViewModel()
|
@State private var viewModel = AddBookmarkViewModel()
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
init(prefilledURL: String? = nil, prefilledTitle: String? = nil) {
|
||||||
|
viewModel.title = prefilledTitle ?? ""
|
||||||
|
viewModel.url = prefilledURL ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
@ -78,6 +83,7 @@ struct AddBookmarkView: View {
|
|||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Abbrechen") {
|
Button("Abbrechen") {
|
||||||
dismiss()
|
dismiss()
|
||||||
|
viewModel.clearForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +91,7 @@ struct AddBookmarkView: View {
|
|||||||
Button("Speichern") {
|
Button("Speichern") {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.createBookmark()
|
await viewModel.createBookmark()
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(!viewModel.isValid || viewModel.isLoading)
|
.disabled(!viewModel.isValid || viewModel.isLoading)
|
||||||
@ -126,6 +133,9 @@ struct AddBookmarkView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.checkClipboard()
|
viewModel.checkClipboard()
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
viewModel.clearForm()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,8 +50,7 @@ class AddBookmarkViewModel {
|
|||||||
// Optional: Zeige die Server-Nachricht an
|
// Optional: Zeige die Server-Nachricht an
|
||||||
print("Server response: \(message)")
|
print("Server response: \(message)")
|
||||||
|
|
||||||
showSuccessAlert = true
|
clearForm()
|
||||||
|
|
||||||
} catch let error as CreateBookmarkError {
|
} catch let error as CreateBookmarkError {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
@ -77,4 +76,10 @@ class AddBookmarkViewModel {
|
|||||||
guard let clipboardURL = clipboardURL else { return }
|
guard let clipboardURL = clipboardURL else { return }
|
||||||
url = clipboardURL
|
url = clipboardURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearForm() {
|
||||||
|
url = ""
|
||||||
|
title = ""
|
||||||
|
labelsText = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,10 @@ struct BookmarksView: View {
|
|||||||
@State private var isScrolling = false
|
@State private var isScrolling = false
|
||||||
let state: BookmarkState
|
let state: BookmarkState
|
||||||
|
|
||||||
|
@State private var showingAddBookmarkFromShare = false
|
||||||
|
@State private var shareURL = ""
|
||||||
|
@State private var shareTitle = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -59,7 +63,7 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle(state.displayName)
|
.navigationTitle(state.displayName)
|
||||||
.sheet(isPresented: $showingAddBookmark) {
|
.sheet(isPresented: $showingAddBookmark) {
|
||||||
AddBookmarkView()
|
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
|
||||||
}
|
}
|
||||||
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||||
Button("OK", role: .cancel) {
|
Button("OK", role: .cancel) {
|
||||||
@ -79,6 +83,9 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("AddBookmarkFromShare"))) { notification in
|
||||||
|
handleShareNotification(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
// Animated FAB Button
|
// Animated FAB Button
|
||||||
@ -107,6 +114,20 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleShareNotification(_ notification: Notification) {
|
||||||
|
guard let userInfo = notification.userInfo,
|
||||||
|
let url = userInfo["url"] as? String,
|
||||||
|
!url.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shareURL = url
|
||||||
|
shareTitle = userInfo["title"] as? String ?? ""
|
||||||
|
showingAddBookmark = true
|
||||||
|
|
||||||
|
print("Received share notification - URL: \(url), Title: \(shareTitle)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper für Scroll-Tracking
|
// Helper für Scroll-Tracking
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class BookmarksViewModel {
|
class BookmarksViewModel {
|
||||||
@ -11,9 +12,39 @@ class BookmarksViewModel {
|
|||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var currentState: BookmarkState = .unread
|
var currentState: BookmarkState = .unread
|
||||||
|
|
||||||
|
var showingAddBookmarkFromShare = false
|
||||||
|
var shareURL = ""
|
||||||
|
var shareTitle = ""
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init() {
|
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
|
@MainActor
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user