Sources/OrgstarShare/ShareViewController.swift
113 lines · 4421 bytes
5 symbols in this file
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