Use atproto actions with ease in iOS shortcuts
at main 9.4 kB view raw
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}