diff --git a/core/Sources/BookmarksCore/Pinboard/Pinboard.swift b/core/Sources/BookmarksCore/Pinboard/Pinboard.swift index bc792add..d99b89b4 100644 --- a/core/Sources/BookmarksCore/Pinboard/Pinboard.swift +++ b/core/Sources/BookmarksCore/Pinboard/Pinboard.swift @@ -211,7 +211,7 @@ public class Pinboard { } } - func postsGet(url: URL) async throws -> Posts { + public func postsGet(url: URL) async throws -> Posts { let requestURL = endpoint(for: .postsGet) .appending(queryItems: [ URLQueryItem(name: "url", value: url.absoluteString) diff --git a/core/Sources/BookmarksCore/Pinboard/Post.swift b/core/Sources/BookmarksCore/Pinboard/Post.swift index 33ba7950..9cdca352 100644 --- a/core/Sources/BookmarksCore/Pinboard/Post.swift +++ b/core/Sources/BookmarksCore/Pinboard/Post.swift @@ -55,7 +55,7 @@ extension Pinboard { meta: String = "", shared: Bool = true, tags: [String] = [], - time: Date = Date(), + time: Date? = Date(), toRead: Bool = false) { self.href = href self.description = description diff --git a/core/Sources/BookmarksCore/Pinboard/Posts.swift b/core/Sources/BookmarksCore/Pinboard/Posts.swift index be8a0794..a295b3ee 100644 --- a/core/Sources/BookmarksCore/Pinboard/Posts.swift +++ b/core/Sources/BookmarksCore/Pinboard/Posts.swift @@ -22,11 +22,11 @@ import Foundation extension Pinboard { - struct Posts: Codable { + public struct Posts: Codable { - let date: Date - let user: String - let posts: [Post] + public let date: Date + public let user: String + public let posts: [Post] } diff --git a/ios/Bookmarks Share Extension/Extensions/NSItemProvider.swift b/ios/Bookmarks Share Extension/Extensions/NSItemProvider.swift deleted file mode 100644 index 0f1d5dc7..00000000 --- a/ios/Bookmarks Share Extension/Extensions/NSItemProvider.swift +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2020-2023 InSeven Limited -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation - -extension NSItemProvider { - - func item(typeIdentifier: String) async throws -> Any? { - try await withCheckedThrowingContinuation { continuation in - loadItem(forTypeIdentifier: typeIdentifier) { object, error in - if let error { - continuation.resume(throwing: error) - return - } - continuation.resume(returning: error) - } - } - } - -} diff --git a/ios/Bookmarks Share Extension/Model/ShareExtensionModel.swift b/ios/Bookmarks Share Extension/Model/ShareExtensionModel.swift index 3a950b64..8ebcbf65 100644 --- a/ios/Bookmarks Share Extension/Model/ShareExtensionModel.swift +++ b/ios/Bookmarks Share Extension/Model/ShareExtensionModel.swift @@ -26,82 +26,60 @@ import Interact import BookmarksCore +protocol ShareExtensionDataSource: NSObject { + + var extensionContext: NSExtensionContext? { get } + +} + // TODO: MainActor assertions class ShareExtensionModel: ObservableObject, Runnable { - static let shared = ShareExtensionModel() - @Published var items: [NSExtensionItem] = [] - @MainActor @Published var urls: [URL] = [] - @MainActor @Published var url: URL? = nil @MainActor @Published var error: Error? = nil @MainActor @Published var post: Pinboard.Post? = nil @MainActor private var cancellables: Set = [] - let pinboard: Pinboard? = { + weak var dataSource: ShareExtensionDataSource? = nil + + var pinboard: Pinboard? { let settings = Settings() guard let apiKey = settings.pinboardApiKey else { return nil } return Pinboard(token: apiKey) - }() - - @MainActor var extensionContext: NSExtensionContext? = nil { - didSet { - dispatchPrecondition(condition: .onQueue(.main)) - guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else { - return - } - urls = [] - extensionItems - .compactMap { $0.attachments } - .reduce([], +) - .filter { $0.hasItemConformingToTypeIdentifier(UTType.url.identifier) } - .forEach { itemProvider in - itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier) { object, error in - DispatchQueue.main.async { [weak self] in - guard let self else { - return - } - guard let url = object as? URL else { - return - } - self.urls.append(url) - } - } - } - } } init() { } - @MainActor func start() { - - // Get the first URL. - $urls - .map { $0.first } - .receive(on: DispatchQueue.main) - .assign(to: \.url, on: self) - .store(in: &cancellables) - - // Create the post with initial values. - $url - .compactMap { $0 } - .asyncMap { url in - do { - let title = try await url.title() - return Pinboard.Post(href: url, description: title ?? "") - } catch { - print("Failed to get contents with error \(error).") - // TODO: - return nil + @MainActor func load() { + dispatchPrecondition(condition: .onQueue(.main)) + guard let extensionItem = dataSource?.extensionContext?.inputItems.first as? NSExtensionItem, + let attachment = extensionItem.attachments?.first + else { + return + } + Task { + guard let url = try await attachment.loadItem(forTypeIdentifier: UTType.url.identifier) as? URL else { + return + } + if let post = try await pinboard?.postsGet(url: url).posts.first { + await MainActor.run { + self.post = post + } + } else { + let title = try await url.title() ?? "" + await MainActor.run { + self.post = Pinboard.Post(href: url, description: title, time: nil) } } - .assign(to: \.post, on: self) - .store(in: &cancellables) + } + } + + @MainActor func start() { } @@ -109,9 +87,6 @@ class ShareExtensionModel: ObservableObject, Runnable { cancellables.removeAll() } - - // TODO: Get the page title; can I do this from the share item? - @MainActor func save(toRead: Bool = false) { guard let pinboard, var post else { @@ -134,11 +109,11 @@ class ShareExtensionModel: ObservableObject, Runnable { } @MainActor func dismiss() { - extensionContext?.completeRequest(returningItems: nil) + dataSource?.extensionContext?.completeRequest(returningItems: nil) } @MainActor func cancel() { - extensionContext?.cancelRequest(withError: CancellationError()) + dataSource?.extensionContext?.cancelRequest(withError: CancellationError()) } } diff --git a/ios/Bookmarks Share Extension/ShareViewController.swift b/ios/Bookmarks Share Extension/ShareViewController.swift index ad2ef3d5..6cf48032 100644 --- a/ios/Bookmarks Share Extension/ShareViewController.swift +++ b/ios/Bookmarks Share Extension/ShareViewController.swift @@ -21,15 +21,20 @@ import SwiftUI import UIKit -class ShareViewController: UIHostingController { +class ShareViewController: UIHostingController, ShareExtensionDataSource { + + let extensionModel: ShareExtensionModel required init?(coder aDecoder: NSCoder) { - super.init(rootView: RootView(extensionModel: ShareExtensionModel.shared)) + let extensionModel = ShareExtensionModel() + self.extensionModel = extensionModel + super.init(rootView: RootView(extensionModel: extensionModel)) + extensionModel.dataSource = self } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - ShareExtensionModel.shared.extensionContext = extensionContext + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + extensionModel.load() } } diff --git a/ios/Bookmarks Share Extension/Views/ContentView.swift b/ios/Bookmarks Share Extension/Views/ContentView.swift index 371c1559..6abbeb33 100644 --- a/ios/Bookmarks Share Extension/Views/ContentView.swift +++ b/ios/Bookmarks Share Extension/Views/ContentView.swift @@ -30,33 +30,34 @@ struct ContentView: View { @EnvironmentObject var extensionModel: ShareExtensionModel var body: some View { - List { + VStack { if let post = Binding($extensionModel.post) { - EditorView(post: post) - } - } - .safeAreaInset(edge: .bottom) { - VStack { - Button { - extensionModel.save() - } label: { - Text("Save") - .frame(maxWidth: .infinity) + List { + EditorView(post: post) } - .buttonStyle(.borderedProminent) - Button { - extensionModel.save(toRead: true) - } label: { - Text("Read Later") - .frame(maxWidth: .infinity) + } else { + PlaceholderView { + ProgressView() } - .buttonStyle(.bordered) } - .controlSize(.large) - .padding() } .navigationTitle("Add Bookmark") .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Menu { + Button("Read Later") { + extensionModel.save(toRead: true) + } + } + label: { + Text("Save") + } primaryAction: { + extensionModel.save() + } + .disabled(extensionModel.post == nil) + } + } .dismissable(.cancel) { extensionModel.dismiss() } diff --git a/ios/Bookmarks Share Extension/Views/EditorView.swift b/ios/Bookmarks Share Extension/Views/EditorView.swift index 490c8af2..004dfb47 100644 --- a/ios/Bookmarks Share Extension/Views/EditorView.swift +++ b/ios/Bookmarks Share Extension/Views/EditorView.swift @@ -37,6 +37,20 @@ struct EditorView: View { return [] } } + if let url = post.href { + Section { + Link(destination: url) { + Text(url.absoluteString) + .multilineTextAlignment(.leading) + } + } header: { + + } footer: { + if let time = post.time { + Text("Created \(time.formatted())") + } + } + } } } diff --git a/ios/Bookmarks Share Extension/Views/RootView.swift b/ios/Bookmarks Share Extension/Views/RootView.swift index 608ff44d..ccda2988 100644 --- a/ios/Bookmarks Share Extension/Views/RootView.swift +++ b/ios/Bookmarks Share Extension/Views/RootView.swift @@ -25,7 +25,7 @@ struct RootView: View { @ObservedObject var extensionModel: ShareExtensionModel var body: some View { - NavigationView { + NavigationStack { ContentView() .environmentObject(extensionModel) } diff --git a/ios/Bookmarks-iOS.xcodeproj/project.pbxproj b/ios/Bookmarks-iOS.xcodeproj/project.pbxproj index ee50ba47..f5e83e73 100644 --- a/ios/Bookmarks-iOS.xcodeproj/project.pbxproj +++ b/ios/Bookmarks-iOS.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ D889F9282A4E19A6000B0AA8 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D889F9272A4E19A6000B0AA8 /* RootView.swift */; }; D889F92B2A4E19EC000B0AA8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D889F92A2A4E19EC000B0AA8 /* ContentView.swift */; }; D889F92E2A4E1A29000B0AA8 /* ShareExtensionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D889F92D2A4E1A29000B0AA8 /* ShareExtensionModel.swift */; }; - D889F9312A4E1A6E000B0AA8 /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D889F9302A4E1A6E000B0AA8 /* NSItemProvider.swift */; }; D889F9332A4E347B000B0AA8 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D889F9322A4E347B000B0AA8 /* EditorView.swift */; }; D89CF2132A4DE8B5000E2B1C /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89CF2122A4DE8B5000E2B1C /* ShareViewController.swift */; }; D89CF2162A4DE8B5000E2B1C /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D89CF2142A4DE8B5000E2B1C /* MainInterface.storyboard */; }; @@ -74,7 +73,6 @@ D889F9272A4E19A6000B0AA8 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; D889F92A2A4E19EC000B0AA8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; D889F92D2A4E1A29000B0AA8 /* ShareExtensionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModel.swift; sourceTree = ""; }; - D889F9302A4E1A6E000B0AA8 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = ""; }; D889F9322A4E347B000B0AA8 /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = ""; }; D89C65E326AF5A8E00D49D11 /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; D89C65E426AF5A8E00D49D11 /* Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; @@ -176,21 +174,12 @@ path = Model; sourceTree = ""; }; - D889F92F2A4E1A54000B0AA8 /* Extensions */ = { - isa = PBXGroup; - children = ( - D889F9302A4E1A6E000B0AA8 /* NSItemProvider.swift */, - ); - path = Extensions; - sourceTree = ""; - }; D89CF2112A4DE8B5000E2B1C /* Bookmarks Share Extension */ = { isa = PBXGroup; children = ( D89CF2212A4E1144000E2B1C /* Bookmarks Share Extension.entitlements */, D89CF2172A4DE8B5000E2B1C /* Info.plist */, D89CF2122A4DE8B5000E2B1C /* ShareViewController.swift */, - D889F92F2A4E1A54000B0AA8 /* Extensions */, D89CF2142A4DE8B5000E2B1C /* MainInterface.storyboard */, D889F92C2A4E1A16000B0AA8 /* Model */, D889F9292A4E19CC000B0AA8 /* Views */, @@ -431,7 +420,6 @@ buildActionMask = 2147483647; files = ( D889F9282A4E19A6000B0AA8 /* RootView.swift in Sources */, - D889F9312A4E1A6E000B0AA8 /* NSItemProvider.swift in Sources */, D889F92E2A4E1A29000B0AA8 /* ShareExtensionModel.swift in Sources */, D889F9332A4E347B000B0AA8 /* EditorView.swift in Sources */, D89CF2132A4DE8B5000E2B1C /* ShareViewController.swift in Sources */,