Use atproto actions with ease in iOS shortcuts
1//
2// ShortcutsView.swift
3// shortcut
4//
5// Created by Bailey Townsend on 6/24/25.
6//
7
8import SwiftUI
9
10struct DocumentationView: View {
11
12 @State private var searchText = ""
13 var intentDocs: [IntentDoc] = []
14 var examples: [Example] = []
15
16 init() {
17 let docs = IntentsDocumentation()
18 intentDocs.append(docs.makeAPostDoc())
19 intentDocs.append(docs.createARecordDoc())
20 intentDocs.append(docs.getServiceAuthDoc())
21 intentDocs.append(docs.createATIDDoc())
22 intentDocs.append(docs.getDownloadBlobsDoc())
23 intentDocs.append(docs.deleteARecordDoc())
24 intentDocs.append(docs.getALocalAtIdentifierDoc())
25 intentDocs.append(docs.getARecordDoc())
26 intentDocs.append(docs.getRepoDoc())
27 intentDocs.append(docs.getListBlobsDoc())
28 intentDocs.append(docs.listRecordsDoc())
29 intentDocs.append(docs.putARecordDoc())
30 intentDocs.append(docs.resolveADidOrHandleDoc())
31 intentDocs.append(docs.createUpdateProfileDoc())
32
33 // intentDocs.append(docs.createUTCDoc())
34 examples = Examples.getExamples()
35 }
36
37 var filteredItems: [IntentDoc] {
38 if searchText.isEmpty {
39 return intentDocs
40 } else {
41 return intentDocs.filter { item in
42 item.name.key.localizedCaseInsensitiveContains(searchText)
43 || item.description.key.localizedCaseInsensitiveContains(searchText)
44 }
45 }
46 }
47
48 var filteredExamples: [Example] {
49 if searchText.isEmpty {
50 return examples
51 } else {
52 return examples.filter { item in
53 item.title.key.localizedCaseInsensitiveContains(searchText)
54 }
55 }
56 }
57
58 var body: some View {
59 NavigationStack {
60 List {
61 Section(header: Text("Actions")) {
62 ForEach(filteredItems, id: \.id) { item in
63 NavigationLink {
64 IntentDocumentationView(intentDoc: item)
65 } label: {
66 Label("\(item.name)", systemImage: item.icon)
67 }
68 }
69 }
70 Section(header: Text("Examples")) {
71 ForEach(filteredExamples, id: \.id) { item in
72 NavigationLink {
73 ExampleView(example: item)
74 // IntentDocumentationView(intentDoc: item)
75 } label: {
76 Label("\(item.title)", systemImage: item.icon)
77 }
78 }
79 }
80 }
81 .navigationTitle("Shortcut actions")
82 .searchable(text: $searchText, prompt: "Search items...")
83 .toolbar {
84 ToolbarItem(placement: .topBarLeading) {
85 Link("Shortcuts", destination: URL(string: "shortcuts://")!)
86 }
87 }
88 }
89 }
90}
91
92struct IntentDocumentationView: View {
93 let intentDoc: IntentDoc
94
95 var body: some View {
96 ScrollView {
97 VStack(alignment: .leading, spacing: 24) {
98 // Header Section
99 headerSection
100
101 // Description Section
102 descriptionSection
103
104 // Parameters Section
105 if !intentDoc.parameters.isEmpty {
106 parametersSection
107 }
108
109 // Result Section
110 if let result = intentDoc.result {
111 resultSection(result)
112 }
113 }
114 .padding()
115 }
116 // .navigationTitle(String(localized: intentDoc.name))
117 // .navigationBarTitleDisplayMode
118 }
119
120 // MARK: - Header Section
121 private var headerSection: some View {
122 HStack(spacing: 16) {
123 Image(systemName: intentDoc.icon)
124 .font(.system(size: 40))
125 .foregroundColor(.accentColor)
126 .frame(width: 60, height: 60)
127 .background(
128 Circle()
129 .fill(Color.accentColor.opacity(0.1))
130 )
131
132 VStack(alignment: .leading, spacing: 4) {
133 Text(intentDoc.name)
134 .font(.title2)
135 .fontWeight(.bold)
136 if let docUrl = intentDoc.docUrl {
137 Link(docUrl.text, destination: docUrl.url)
138 }
139 // Text("Intent Documentation")
140 // .font(.caption)
141 // .foregroundColor(.secondary)
142 }
143
144 Spacer()
145 }
146 .padding(.vertical, 8)
147 }
148
149 // MARK: - Description Section
150 private var descriptionSection: some View {
151 DocumentationSection(title: "Description", icon: "doc.text") {
152 Text(intentDoc.description)
153 .font(.body)
154 .foregroundColor(.primary)
155 }
156 }
157
158 // MARK: - Parameters Section
159 private var parametersSection: some View {
160 DocumentationSection(title: "Parameters", icon: "list.bullet") {
161 LazyVStack(spacing: 12) {
162 ForEach(Array(intentDoc.parameters.enumerated()), id: \.offset) {
163 index, parameter in
164 ParameterCard(parameter: parameter)
165 }
166 }
167 }
168 }
169
170 // MARK: - Result Section
171 private func resultSection(_ result: AppEntityResult) -> some View {
172 DocumentationSection(title: "Result", icon: "return") {
173 VStack(alignment: .leading, spacing: 16) {
174 // Result Overview
175 VStack(alignment: .leading, spacing: 8) {
176 Text(result.name)
177 .font(.headline)
178 .foregroundColor(.primary)
179
180 Text(result.description)
181 .font(.body)
182 .foregroundColor(.secondary)
183 }
184 .padding()
185 .background(
186 RoundedRectangle(cornerRadius: 12)
187 .fill(Color(.systemGray6))
188 )
189
190 // Result Parameters
191 if !result.parameters.isEmpty {
192 VStack(alignment: .leading, spacing: 12) {
193 Text("Result Properties")
194 .font(.subheadline)
195 .fontWeight(.semibold)
196 .foregroundColor(.primary)
197
198 ForEach(Array(result.parameters), id: \.id) { parameter in
199 ParameterCard(parameter: parameter, isCompact: true)
200 }
201 }
202 }
203 }
204 }
205 }
206}
207
208// MARK: - Supporting Views
209
210struct DocumentationSection<Content: View>: View {
211 let title: String
212 let icon: String
213 let content: Content
214
215 init(title: String, icon: String, @ViewBuilder content: () -> Content) {
216 self.title = title
217 self.icon = icon
218 self.content = content()
219 }
220
221 var body: some View {
222 VStack(alignment: .leading, spacing: 16) {
223 HStack(spacing: 8) {
224 Image(systemName: icon)
225 .foregroundColor(.accentColor)
226 .font(.system(size: 16, weight: .medium))
227
228 Text(title)
229 .font(.title3)
230 .fontWeight(.semibold)
231
232 Spacer()
233 }
234
235 content
236 }
237 .padding()
238 .background(
239 RoundedRectangle(cornerRadius: 16)
240 .fill(Color(.systemBackground))
241 .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 2)
242 )
243 }
244}
245
246struct ParameterCard: View {
247 let parameter: IntentParameter
248 let isCompact: Bool
249
250 init(parameter: IntentParameter, isCompact: Bool = false) {
251 self.parameter = parameter
252 self.isCompact = isCompact
253 }
254
255 var body: some View {
256 VStack(alignment: .leading, spacing: isCompact ? 6 : 8) {
257 HStack {
258 Text(parameter.name)
259 .font(isCompact ? .subheadline : .headline)
260 .fontWeight(.medium)
261 .foregroundColor(.primary)
262
263 Spacer()
264
265 Text(parameter.type)
266 .font(.caption)
267 .fontWeight(.medium)
268 .foregroundColor(.white)
269 .padding(.horizontal, 8)
270 .padding(.vertical, 4)
271 .background(
272 Capsule()
273 .fill(Color.accentColor)
274 )
275 }
276 if let description = parameter.description {
277 Text(description)
278 .font(isCompact ? .caption : .body)
279 .foregroundColor(.secondary)
280 .fixedSize(horizontal: false, vertical: true)
281 }
282
283 }
284 .padding(isCompact ? 12 : 16)
285 .background(
286 RoundedRectangle(cornerRadius: 12)
287 .fill(Color(.systemGray6))
288 )
289 }
290}
291#Preview {
292 DocumentationView()
293}