From dcbe0515fc27d58454f850dfbeac124fe88b5824 Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Wed, 17 Sep 2025 22:27:52 +0200 Subject: [PATCH] fix: Share extension title extraction and theme persistence - Enable text support in share extension to extract page titles - Extract titles from attributedTitle and attributedContentText - Prevent titles from being used as URLs with proper validation - Fix theme settings persistence using SettingsRepository instead of UserDefaults - Theme changes now properly notify the app for immediate updates --- URLShare/Info.plist | 2 ++ URLShare/ShareBookmarkViewModel.swift | 35 +++++++++++++++++-- .../UI/Settings/AppearanceSettingsView.swift | 25 +++++++++---- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/URLShare/Info.plist b/URLShare/Info.plist index ea3b19a..33dc9eb 100644 --- a/URLShare/Info.plist +++ b/URLShare/Info.plist @@ -8,6 +8,8 @@ NSExtensionActivationRule + NSExtensionActivationSupportsText + NSExtensionActivationSupportsWebURLWithMaxCount 1 diff --git a/URLShare/ShareBookmarkViewModel.swift b/URLShare/ShareBookmarkViewModel.swift index 97c4dac..ad22b9e 100644 --- a/URLShare/ShareBookmarkViewModel.swift +++ b/URLShare/ShareBookmarkViewModel.swift @@ -67,8 +67,22 @@ class ShareBookmarkViewModel: ObservableObject { logger.warning("No extension context available for content extraction") return } + + var extractedUrl: String? + var extractedTitle: String? + for item in extensionContext.inputItems { guard let inputItem = item as? NSExtensionItem else { continue } + + // Use the inputItem's attributedTitle or attributedContentText as potential title + if let attributedTitle = inputItem.attributedTitle?.string, !attributedTitle.isEmpty { + extractedTitle = attributedTitle + logger.info("Extracted title from input item: \(attributedTitle)") + } else if let attributedContent = inputItem.attributedContentText?.string, !attributedContent.isEmpty { + extractedTitle = attributedContent + logger.info("Extracted title from content text: \(attributedContent)") + } + for attachment in inputItem.attachments ?? [] { if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) { attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (url, error) in @@ -76,6 +90,12 @@ class ShareBookmarkViewModel: ObservableObject { if let url = url as? URL { self?.url = url.absoluteString self?.logger.info("Extracted URL from shared content: \(url.absoluteString)") + + // Set title if we extracted one and current title is empty + if let title = extractedTitle, self?.title.isEmpty == true { + self?.title = title + self?.logger.info("Set title from shared content: \(title)") + } } else if let error = error { self?.logger.error("Failed to extract URL: \(error.localizedDescription)") } @@ -85,9 +105,18 @@ class ShareBookmarkViewModel: ObservableObject { if attachment.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { attachment.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (text, error) in DispatchQueue.main.async { - if let text = text as? String, let url = URL(string: text) { - self?.url = url.absoluteString - self?.logger.info("Extracted URL from shared text: \(url.absoluteString)") + if let text = text as? String { + // Only treat as URL if it's a valid URL and we don't have one yet + if self?.url == nil, let url = URL(string: text), url.scheme != nil { + self?.url = url.absoluteString + self?.logger.info("Extracted URL from shared text: \(url.absoluteString)") + } else { + // If not a valid URL or we already have a URL, treat as potential title + if self?.title.isEmpty == true { + self?.title = text + self?.logger.info("Set title from shared text: \(text)") + } + } } else if let error = error { self?.logger.error("Failed to extract text: \(error.localizedDescription)") } diff --git a/readeck/UI/Settings/AppearanceSettingsView.swift b/readeck/UI/Settings/AppearanceSettingsView.swift index 3d77e55..4982808 100644 --- a/readeck/UI/Settings/AppearanceSettingsView.swift +++ b/readeck/UI/Settings/AppearanceSettingsView.swift @@ -6,10 +6,12 @@ struct AppearanceSettingsView: View { private let loadCardLayoutUseCase: PLoadCardLayoutUseCase private let saveCardLayoutUseCase: PSaveCardLayoutUseCase + private let settingsRepository: PSettingsRepository init(factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.loadCardLayoutUseCase = factory.makeLoadCardLayoutUseCase() self.saveCardLayoutUseCase = factory.makeSaveCardLayoutUseCase() + self.settingsRepository = SettingsRepository() } var body: some View { @@ -58,18 +60,29 @@ struct AppearanceSettingsView: View { } private func loadSettings() { - // Load theme setting - let themeString = UserDefaults.standard.string(forKey: "selectedTheme") ?? "system" - selectedTheme = Theme(rawValue: themeString) ?? .system - - // Load card layout setting Task { + // Load both theme and card layout from repository + if let settings = try? await settingsRepository.loadSettings() { + await MainActor.run { + selectedTheme = settings.theme ?? .system + } + } selectedCardLayout = await loadCardLayoutUseCase.execute() } } private func saveThemeSettings() { - UserDefaults.standard.set(selectedTheme.rawValue, forKey: "selectedTheme") + Task { + // Load current settings, update theme, and save back + var settings = (try? await settingsRepository.loadSettings()) ?? Settings() + settings.theme = selectedTheme + try? await settingsRepository.saveSettings(settings) + + // Notify app about theme change + await MainActor.run { + NotificationCenter.default.post(name: .settingsChanged, object: nil) + } + } } private func saveCardLayoutSettings() {