Use atproto actions with ease in iOS shortcuts
at main 7.5 kB view raw
1// 2// AccountsView.swift 3// shortcut 4// 5// Created by Bailey Townsend on 6/24/25. 6// 7 8import ATProtoKit 9import SwiftData 10import SwiftUI 11 12struct AccountsView: View { 13 14 @EnvironmentObject var atProtocolManager: AtProtocolManager 15 @Query(sort: [SortDescriptor(\UserSessionModel.handle, comparator: .localizedStandard)]) 16 var userSessions: [UserSessionModel] 17 18 @State var itemToDelete: UserSessionModel? = nil 19 @State private var showDeleteConfirmation: Bool = false 20 @State private var errorMessage: String? 21 @State private var showAlert = false 22 23 var body: some View { 24 NavigationStack { 25 Group { 26 if self.userSessions.isEmpty { 27 VStack(spacing: 16) { 28 Image(systemName: "person.3") 29 .font(.system(size: 50)) 30 .foregroundColor(.gray) 31 32 Text("No ATProto Accounts Yet") 33 .font(.title2) 34 .fontWeight(.medium) 35 .foregroundColor(.primary) 36 37 Text("Add your first account to get started") 38 .font(.body) 39 .foregroundColor(.secondary) 40 .multilineTextAlignment(.center) 41 } 42 .padding() 43 } else { 44 List { 45 ForEach(self.userSessions) { session in 46 AccountRow( 47 handle: session.handle, profilePicture: session.profilePicture 48 ) 49 .swipeActions { 50 Button { 51 Task { 52 await self.refreshSession(session: session) 53 } 54 } label: { 55 Label("Refresh", systemImage: "arrow.clockwise.circle") 56 } 57 Button(role: .destructive) { 58 showDeleteConfirmation = true 59 itemToDelete = session 60 } label: { 61 Label("Delete", systemImage: "trash.fill") 62 } 63 64 } 65 } 66 67 } 68 } 69 } 70 .navigationTitle("Accounts") 71 .toolbar { 72 ToolbarItem(placement: .navigationBarTrailing) { 73 NavigationLink { 74 LoginView() 75 .environmentObject(atProtocolManager) 76 } label: { 77 Image(systemName: "plus") 78 } 79 } 80 } 81 .alert(isPresented: $showAlert) { 82 Alert( 83 title: Text("Error"), message: Text(errorMessage ?? "Unknown error"), 84 dismissButton: .default(Text("OK"))) 85 } 86 87 } 88 .confirmationDialog( 89 Text( 90 "Are you sure you want to remove the handle \(self.itemToDelete?.handle ?? "")? You will no longer be able to access this account in your shortcuts." 91 ), 92 isPresented: $showDeleteConfirmation, 93 titleVisibility: .visible 94 ) { 95 Button("Delete", role: .destructive) { 96 withAnimation { 97 if let session = itemToDelete { 98 99 deleteAccount(id: session.id) 100 itemToDelete = nil 101 } 102 } 103 } 104 } 105 } 106 107 private func deleteAccount(id: UUID) { 108 Task { 109 do { 110 try await self.atProtocolManager.deleteSession(sessionID: id) 111 } catch { 112 self.errorMessage = error.localizedDescription 113 self.showAlert = true 114 } 115 } 116 } 117 118 private func refreshSession(session: UserSessionModel) async { 119 do { 120 let atProtoKit = try await self.atProtocolManager.getAtProtoKit(sessionModel: session) 121 try await self.atProtocolManager.refreshUserSession(atProtoKit) 122 } catch { 123 self.errorMessage = error.localizedDescription 124 self.showAlert = true 125 } 126 127 } 128 129} 130 131struct AccountRow: View { 132 let handle: String 133 let profilePicture: URL? 134 135 var body: some View { 136 HStack(spacing: 12) { 137 // Profile picture circle 138 Group { 139 if let imageURL = self.profilePicture { 140 AsyncImage(url: imageURL) { image in 141 image 142 .resizable() 143 .aspectRatio(contentMode: .fill) 144 } placeholder: { 145 Circle() 146 .fill(.blue.gradient) 147 .overlay { 148 Text(String(self.handle.prefix(1).uppercased())) 149 .font(.headline) 150 .fontWeight(.medium) 151 .foregroundColor(.white) 152 } 153 } 154 } else { 155 Circle() 156 .fill(.blue.gradient) 157 .overlay { 158 Text(String(self.handle.prefix(1).uppercased())) 159 .font(.headline) 160 .fontWeight(.medium) 161 .foregroundColor(.white) 162 } 163 } 164 } 165 .frame(width: 44, height: 44) 166 .clipShape(Circle()) 167 168 VStack(alignment: .leading, spacing: 2) { 169 Text(self.handle) 170 .font(.headline) 171 .foregroundColor(.primary) 172 173 } 174 175 Spacer() 176 } 177 .padding(.vertical, 4) 178 } 179} 180 181#Preview { 182 183 let configuration = ModelConfiguration(isStoredInMemoryOnly: true) 184 let container = try! ModelContainer( 185 for: UserSessionModel.self, 186 configurations: configuration 187 ) 188 // var userSessions: [UserSessionModel] = [ 189 // UserSessionModel( 190 // sessionId: UUID(), handle: "alice.bsky.social", sessionDID: "", 191 // serviceEndpoint: URL(string: "https://localhost")!, 192 // profilePicture: URL( 193 // string: "https://www.placeholderimage.online/images/avatar/avatar-image-22.png")!), 194 // UserSessionModel( 195 // sessionId: UUID(), handle: "jonah.bsky.social", sessionDID: "", 196 // serviceEndpoint: URL(string: "https://localhost")!, 197 // profilePicture: URL( 198 // string: "https://www.placeholderimage.online/images/avatar/avatar-image-05.png")!), 199 // UserSessionModel( 200 // sessionId: UUID(), handle: "earl.bsky.social", sessionDID: "", 201 // serviceEndpoint: URL(string: "https://localhost")!, 202 // ), 203 // ] 204 205 AccountsView() 206 .environmentObject(AtProtocolManager(modelContext: container.mainContext)) 207}