krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgstarShare/ShareViewController.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Sources/OrgstarShare/ShareViewController.swift history · blame · raw

112 lines · 4409 bytes

  1#if os(iOS)
  2import OrgApp
  3import OrgCore
  4import SwiftUI
  5import UIKit
  6import UniformTypeIdentifiers
  7
  8/// The share sheet's Orgstar: the shared page or text with a template and a note, left for the
  9/// app, which opens its capture sheet with them the next time it's active.
 10@objc(ShareViewController)
 11final class ShareViewController: UIViewController {
 12    override func viewDidLoad() {
 13        super.viewDidLoad()
 14        let items = extensionContext?.inputItems as? [NSExtensionItem] ?? []
 15        Task { @MainActor in
 16            let shared = await Shared.load(items)
 17            let inbox = CaptureInbox()
 18            let form = ShareForm(shared: shared, templates: inbox?.templates() ?? [], available: inbox != nil) { [weak self] link in
 19                if let link { try? inbox?.add(link) }
 20                self?.extensionContext?.completeRequest(returningItems: nil)
 21            }
 22            let host = UIHostingController(rootView: form)
 23            addChild(host)
 24            host.view.frame = view.bounds
 25            host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
 26            view.addSubview(host.view)
 27            host.didMove(toParent: self)
 28        }
 29    }
 30}
 31
 32/// What the other app shared: a page's address and title, or text.
 33struct Shared {
 34    var url: URL?
 35    var title = ""
 36    var text = ""
 37
 38    @MainActor
 39    static func load(_ items: [NSExtensionItem]) async -> Shared {
 40        var shared = Shared()
 41        for item in items {
 42            if shared.title.isEmpty, let title = item.attributedTitle?.string ?? item.attributedContentText?.string { shared.title = title }
 43            for provider in item.attachments ?? [] {
 44                if shared.url == nil, provider.hasItemConformingToTypeIdentifier(UTType.url.identifier),
 45                   let url = try? await provider.loadItem(forTypeIdentifier: UTType.url.identifier) as? URL {
 46                    shared.url = url
 47                } else if shared.text.isEmpty, provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier),
 48                          let text = try? await provider.loadItem(forTypeIdentifier: UTType.plainText.identifier) as? String {
 49                    shared.text = text
 50                }
 51            }
 52        }
 53        // Text shared on its own arrives as the item's content too.
 54        if shared.url == nil, shared.text == shared.title { shared.title = "" }
 55        return shared
 56    }
 57}
 58
 59struct ShareForm: View {
 60    let shared: Shared
 61    let templates: [CaptureInbox.Template]
 62    let available: Bool
 63    let finish: (String?) -> Void
 64    @State private var title = ""
 65    @State private var note = ""
 66    @State private var template: String?
 67
 68    var body: some View {
 69        NavigationStack {
 70            Form {
 71                if !available {
 72                    Text("Orgstar's shared folder isn't available.").foregroundStyle(.red)
 73                }
 74                if !templates.isEmpty {
 75                    Picker("Template", selection: $template) {
 76                        ForEach(templates, id: \.key) { Text($0.name).tag(Optional($0.key)) }
 77                    }
 78                }
 79                if let url = shared.url {
 80                    Section("Link") {
 81                        TextField("Title", text: $title)
 82                        Text(url.absoluteString).font(.footnote).foregroundStyle(.secondary).lineLimit(2)
 83                    }
 84                }
 85                Section("Text") {
 86                    TextEditor(text: $note).frame(minHeight: 120)
 87                }
 88                Section {
 89                } footer: {
 90                    Text("Orgstar files it with the template when you next open it.")
 91                }
 92            }
 93            .navigationTitle("Orgstar")
 94            .navigationBarTitleDisplayMode(.inline)
 95            .toolbar {
 96                ToolbarItem(placement: .cancellationAction) { Button("Cancel") { finish(nil) } }
 97                ToolbarItem(placement: .confirmationAction) {
 98                    Button("Capture") {
 99                        finish(OrgProtocol.captureURL(template: template, url: shared.url?.absoluteString, title: title, body: note))
100                    }
101                    .disabled(!available)
102                }
103            }
104        }
105        .onAppear {
106            title = shared.title
107            note = shared.text
108            template = templates.first?.key
109        }
110    }
111}
112#endif