import SwiftUI struct SettingsView: View { let accountStore: AccountStore @State private var showAddAccount = false var body: some View { VStack(spacing: 0) { // Header HStack { Text("Accounts") .font(.headline) Spacer() Button { showAddAccount = true } label: { Image(systemName: "plus") } } .padding() Divider() // Account list if accountStore.accounts.isEmpty { VStack(spacing: 8) { Image(systemName: "person.crop.circle.badge.plus") .font(.largeTitle) .foregroundStyle(.secondary) Text("No accounts yet") .font(.subheadline) .foregroundStyle(.secondary) Text("Add a Mastodon, WordPress, or Micro.blog account to get started.") .font(.caption) .foregroundStyle(.tertiary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } else { List { ForEach(accountStore.accounts) { account in HStack { Image(systemName: account.serviceType.iconName) .frame(width: 20) VStack(alignment: .leading, spacing: 2) { Text(account.displayName) .font(.body) Text(account.serviceType.displayName) .font(.caption) .foregroundStyle(.secondary) } Spacer() Button(role: .destructive) { accountStore.removeAccount(account) } label: { Image(systemName: "trash") .font(.caption) } .buttonStyle(.plain) .foregroundStyle(.red.opacity(0.7)) } .padding(.vertical, 2) } } } } .frame(width: 360, height: 300) .sheet(isPresented: $showAddAccount) { AddAccountView(accountStore: accountStore, isPresented: $showAddAccount) } } } struct AddAccountView: View { let accountStore: AccountStore @Binding var isPresented: Bool @State private var selectedService: ServiceType = .mastodon @State private var instanceURL = "" @State private var token = "" @State private var username = "" @State private var isLoading = false @State private var errorMessage: String? var body: some View { VStack(spacing: 16) { Text("Add Account") .font(.headline) // Service picker Picker("Service", selection: $selectedService) { ForEach(ServiceType.allCases) { service in Text(service.displayName).tag(service) } } .pickerStyle(.segmented) // Service-specific fields switch selectedService { case .mastodon: mastodonFields case .wordpress: wordpressFields case .microblog: microblogFields } if let errorMessage { Text(errorMessage) .font(.caption) .foregroundStyle(.red) } HStack { Button("Cancel") { isPresented = false } .keyboardShortcut(.cancelAction) Spacer() if isLoading { ProgressView() .controlSize(.small) } Button("Add") { Task { await addAccount() } } .buttonStyle(.borderedProminent) .disabled(!canAdd || isLoading) .keyboardShortcut(.defaultAction) } } .padding() .frame(width: 360) } private var canAdd: Bool { switch selectedService { case .mastodon: !instanceURL.trimmingCharacters(in: .whitespaces).isEmpty case .wordpress: !instanceURL.trimmingCharacters(in: .whitespaces).isEmpty && !username.trimmingCharacters(in: .whitespaces).isEmpty && !token.trimmingCharacters(in: .whitespaces).isEmpty case .microblog: !token.trimmingCharacters(in: .whitespaces).isEmpty } } @ViewBuilder private var mastodonFields: some View { VStack(alignment: .leading, spacing: 4) { Text("Instance URL") .font(.caption) .foregroundStyle(.secondary) TextField("https://mastodon.social", text: $instanceURL) .textFieldStyle(.roundedBorder) } Text("You'll be redirected to your instance to authorize qStatus.") .font(.caption) .foregroundStyle(.secondary) } @ViewBuilder private var wordpressFields: some View { VStack(alignment: .leading, spacing: 4) { Text("Site URL") .font(.caption) .foregroundStyle(.secondary) TextField("https://your-site.com", text: $instanceURL) .textFieldStyle(.roundedBorder) } VStack(alignment: .leading, spacing: 4) { Text("Username") .font(.caption) .foregroundStyle(.secondary) TextField("admin", text: $username) .textFieldStyle(.roundedBorder) } VStack(alignment: .leading, spacing: 4) { Text("Application Password") .font(.caption) .foregroundStyle(.secondary) SecureField("xxxx xxxx xxxx xxxx", text: $token) .textFieldStyle(.roundedBorder) } Text("Generate an Application Password in WordPress: Users > Profile > Application Passwords") .font(.caption) .foregroundStyle(.secondary) } @ViewBuilder private var microblogFields: some View { VStack(alignment: .leading, spacing: 4) { Text("App Token") .font(.caption) .foregroundStyle(.secondary) SecureField("Paste your token", text: $token) .textFieldStyle(.roundedBorder) } Text("Get your token at micro.blog/account/apps") .font(.caption) .foregroundStyle(.secondary) } private func addAccount() async { isLoading = true errorMessage = nil do { switch selectedService { case .mastodon: try await addMastodonAccount() case .wordpress: try await addWordPressAccount() case .microblog: try await addMicroblogAccount() } isPresented = false } catch { errorMessage = error.localizedDescription } isLoading = false } private func addMastodonAccount() async throws { var url = instanceURL.trimmingCharacters(in: .whitespaces) if !url.hasPrefix("https://") && !url.hasPrefix("http://") { url = "https://\(url)" } let (clientID, clientSecret) = try await MastodonClient.registerApp(instanceURL: url) // Store client credentials temporarily var account = Account( serviceType: .mastodon, displayName: url.replacingOccurrences(of: "https://", with: ""), instanceURL: url, username: "" ) account.mastodonClientID = clientID account.mastodonClientSecret = clientSecret // Save account without token - will complete after OAuth let accountData = try JSONEncoder().encode(account) UserDefaults.standard.set(accountData, forKey: "qstatus.pending-mastodon-account") // Open browser for authorization let authURL = MastodonClient.authorizeURL(instanceURL: url, clientID: clientID) NSWorkspace.shared.open(authURL) } private func addWordPressAccount() async throws { var url = instanceURL.trimmingCharacters(in: .whitespaces) if !url.hasPrefix("https://") && !url.hasPrefix("http://") { url = "https://\(url)" } let user = username.trimmingCharacters(in: .whitespaces) let pass = token.trimmingCharacters(in: .whitespaces) let valid = try await WordPressClient.verifyCredentials( siteURL: url, username: user, password: pass ) guard valid else { throw PostingError(message: "Invalid credentials. Check username and application password.") } let credentials = "\(user):\(pass)" let account = Account( serviceType: .wordpress, displayName: "\(user)@\(url.replacingOccurrences(of: "https://", with: ""))", instanceURL: url, username: user ) try accountStore.addAccount(account, token: credentials) } private func addMicroblogAccount() async throws { let appToken = token.trimmingCharacters(in: .whitespaces) let blogName = try await MicroblogClient.verifyToken(appToken) let account = Account( serviceType: .microblog, displayName: blogName, instanceURL: "https://micro.blog", username: blogName ) try accountStore.addAccount(account, token: appToken) } }