oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import RastrilloApple
2import SwiftUI
3
4@MainActor struct DocsSharingView: View {
5 let client: NativeHTTP
6 let sqid: String
7 @Environment(\.dismiss) private var dismiss
8 @State private var information: DocsShareInformation?
9 @State private var address = ""
10 @State private var role = "reader"
11 @State private var busy = false
12 @State private var needsRefresh = true
13 @State private var errorKey: String?
14 @State private var oneTimeURL: URL?
15
16 var body: some View {
17 NavigationStack {
18 Form {
19 if let errorKey {
20 Section {
21 Text(LocalizedStringKey(errorKey), bundle: DocsResources.bundle)
22 Button { Task { await refresh() } } label: { label("native.retry") }
23 .disabled(busy)
24 }
25 }
26 if busy { ProgressView() }
27 if let information {
28 Section {
29 if information.grants.isEmpty { label("docs.share.empty") }
30 ForEach(information.grants) { grant in
31 VStack(alignment: .leading) {
32 Text(verbatim: grant.address).textSelection(.enabled)
33 label(grant.role == "writer" ? "docs.share.role.writer" : "docs.share.role.reader")
34 .foregroundStyle(.secondary)
35 Button(role: .destructive) {
36 Task { await change("/revoke", values: ["address": grant.address]) }
37 } label: { label("docs.share.revoke") }
38 .disabled(busy || needsRefresh)
39 }
40 }
41 } header: { label("docs.share.heading") }
42 Section {
43 TextField(String(localized: "docs.share.address", bundle: DocsResources.bundle), text: $address)
44 .autocorrectionDisabled()
45 #if os(iOS)
46 .textInputAutocapitalization(.never)
47 .keyboardType(.emailAddress)
48 #endif
49 Picker(selection: $role) {
50 label("docs.share.role.reader").tag("reader")
51 label("docs.share.role.writer").tag("writer")
52 } label: { label("docs.share.role") }
53 Button { Task { await change("", values: ["address": address, "role": role]) } } label: { label("docs.share.submit") }
54 .disabled(busy || needsRefresh || address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
55 }.disabled(busy)
56 Section {
57 label("docs.share.public.body")
58 if let oneTimeURL {
59 Text(verbatim: oneTimeURL.absoluteString).textSelection(.enabled)
60 ShareLink(item: oneTimeURL) { label("docs.share.submit") }
61 label("docs.share.public.once").foregroundStyle(.secondary)
62 } else if information.published {
63 label("docs.share.public.published").foregroundStyle(.secondary)
64 }
65 if information.published {
66 Button { Task { await change("/link", values: ["on": "1"]) } } label: { label("docs.share.public.replace") }
67 Button(role: .destructive) { Task { await change("/link", values: ["on": "0"]) } } label: { label("docs.share.public.off") }
68 } else {
69 Button { Task { await change("/link", values: ["on": "1"]) } } label: { label("docs.share.public.on") }
70 }
71 } header: { label("docs.share.public.heading") }
72 footer: { label("docs.share.public.hint") }
73 .disabled(busy || needsRefresh)
74 }
75 }
76 .formStyle(.grouped)
77 .navigationTitle(Text("docs.share.title", bundle: DocsResources.bundle))
78 .toolbar { ToolbarItem(placement: .confirmationAction) {
79 Button { dismiss() } label: { label("docs.share.back") }.disabled(busy)
80 } }
81 .task(id: sqid) { await refresh() }
82 .interactiveDismissDisabled(busy)
83 #if os(macOS)
84 .frame(minWidth: 460, idealWidth: 540, minHeight: 520)
85 #endif
86 }
87 }
88
89 private func label(_ key: String) -> Text { Text(LocalizedStringKey(key), bundle: DocsResources.bundle) }
90
91 private func load() async throws {
92 let state: DocsShareInformation = try await client.request("/d/\(sqid)/share")
93 information = state
94 if !state.published { oneTimeURL = nil }
95 else if !state.public_url.isEmpty { oneTimeURL = URL(string: state.public_url) }
96 needsRefresh = false
97 }
98
99 private func refresh() async {
100 guard !busy else { return }
101 busy = true
102 defer { busy = false }
103 errorKey = nil
104 do { try await load() }
105 catch { needsRefresh = true; errorKey = "native.share.loadFailed" }
106 }
107
108 private func change(_ suffix: String, values: [String: String]) async {
109 guard !busy && !needsRefresh else { return }
110 busy = true
111 errorKey = nil
112 defer { busy = false }
113 // URL query encoding leaves '+' literal; forms interpret it as space.
114 var components = URLComponents()
115 components.queryItems = values.sorted { $0.key < $1.key }.map { URLQueryItem(name: $0.key, value: $0.value) }
116 let body = Data((components.percentEncodedQuery ?? "").replacingOccurrences(of: "+", with: "%2B").utf8)
117 do {
118 let result: DocsShareMutation = try await client.request("/d/\(sqid)/share\(suffix)", method: "POST", body: body, contentType: "application/x-www-form-urlencoded")
119 if suffix == "/link" {
120 guard let published = result.published else { throw NativeClientError.message("Invalid sharing response") }
121 oneTimeURL = published ? result.public_url.flatMap(URL.init(string:)) : nil
122 } else {
123 guard result.ok == true else { throw NativeClientError.message("Invalid sharing response") }
124 if suffix.isEmpty { address = "" }
125 }
126 do { try await load() }
127 catch { needsRefresh = true; errorKey = "native.share.loadFailed" }
128 } catch { needsRefresh = true; errorKey = "native.share.saveFailed" }
129 }
130}
131