krz/orgstar

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

Sources/OrgstarShare/ShareViewController.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Sources/OrgstarShare/ShareViewController.swift history · blame · raw

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