Sources/OrgstarShare/ShareViewController.swift
112 lines · 4409 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 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