import SwiftUI struct IOSSettingsView: View { @EnvironmentObject private var appState: AppState @EnvironmentObject private var workspaceStore: WorkspaceStore @State private var editor: ServerEditorItem? @State private var serverToDelete: SonarServer? var body: some View { NavigationStack { Form { Section("Workspace") { LabeledContent("Name", value: workspaceStore.selectedWorkspace.name) LabeledContent("Type", value: workspaceStore.selectedWorkspace.kind == .team ? "Team" : "Personal") LabeledContent("Role", value: workspaceStore.selectedWorkspace.role.rawValue.capitalized) } Section { if appState.servers.isEmpty { ContentUnavailableView("No SonarQube Servers", systemImage: "server.rack", description: Text("Add a server to load your SonarQube projects.")) } else { ForEach(appState.servers) { server in Button { appState.selectServer(server.id) } label: { HStack(spacing: 12) { Image(systemName: appState.selectedServerID == server.id ? "checkmark.circle.fill" : "circle") .foregroundStyle(appState.selectedServerID == server.id ? Color.accentColor : Color.secondary) VStack(alignment: .leading, spacing: 3) { Text(server.name).foregroundStyle(.primary) Text(server.baseURL).font(.caption).foregroundStyle(.secondary).lineLimit(1) } Spacer() Button { editor = ServerEditorItem(server: server) } label: { Image(systemName: "pencil") } .buttonStyle(.borderless) } } .swipeActions(edge: .trailing) { Button(role: .destructive) { serverToDelete = server } label: { Label("Delete", systemImage: "trash") } Button { editor = ServerEditorItem(server: server) } label: { Label("Edit", systemImage: "pencil") }.tint(.blue) } } } Button { editor = ServerEditorItem(server: nil) } label: { Label("Add SonarQube Server", systemImage: "plus.circle.fill") } } header: { Text("SonarQube Servers") } footer: { Text("Access tokens are stored only in this device's Keychain and are not written to iCloud.") } if let server = appState.selectedServer { Section("Active Server") { LabeledContent("Server", value: server.name) LabeledContent("Address", value: server.baseURL) if let version = appState.serverVersion { LabeledContent("Version", value: version) } Button("Refresh Projects") { Task { await appState.refresh() } } } } } .navigationTitle("Settings") .sheet(item: $editor) { item in IOSServerEditorView(server: item.server) .environmentObject(appState) } .confirmationDialog("Delete this server?", isPresented: Binding(get: { serverToDelete != nil }, set: { if !$0 { serverToDelete = nil } }), titleVisibility: .visible) { if let server = serverToDelete { Button("Delete \(server.name)", role: .destructive) { appState.removeServer(server); serverToDelete = nil } } Button("Cancel", role: .cancel) { serverToDelete = nil } } message: { Text("The server configuration and its token will be removed from this device.") } } } } private struct ServerEditorItem: Identifiable { let id = UUID() let server: SonarServer? } private struct IOSServerEditorView: View { @EnvironmentObject private var appState: AppState @Environment(\.dismiss) private var dismiss let server: SonarServer? @State private var name: String @State private var url: String @State private var token = "" @State private var isTesting = false @State private var isSaving = false @State private var testResult: String? @State private var errorMessage: String? init(server: SonarServer?) { self.server = server _name = State(initialValue: server?.name ?? "") _url = State(initialValue: server?.baseURL ?? "https://") } var body: some View { NavigationStack { Form { Section("Server") { TextField("Name", text: $name).textContentType(.organizationName) TextField("https://sonarqube.example.com", text: $url) .textInputAutocapitalization(.never).autocorrectionDisabled() .keyboardType(.URL).textContentType(.URL) } Section("Authentication") { SecureField(server == nil ? "SonarQube API token" : "New token (leave blank to keep current)", text: $token) .textInputAutocapitalization(.never).autocorrectionDisabled() Text(server == nil ? "Create a SonarQube user token and paste it here." : "For security the existing token cannot be displayed. Leave this blank unless you want to replace it.") .font(.footnote).foregroundStyle(.secondary) } Section { Button { Task { await testConnection() } } label: { HStack { if isTesting { ProgressView() }; Label("Test Connection", systemImage: "network") } }.disabled(isTesting || url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (server == nil && token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)) if let testResult { Label(testResult, systemImage: "checkmark.circle.fill").foregroundStyle(.green) } if let errorMessage { Text(errorMessage).foregroundStyle(.red).font(.footnote) } } } .navigationTitle(server == nil ? "Add Server" : "Edit Server") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { save() }.disabled(isSaving || url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (server == nil && token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)) } } } } @MainActor private func testConnection() async { isTesting = true; errorMessage = nil; testResult = nil defer { isTesting = false } do { let result: String if let server, token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { result = try await appState.testServer(id: server.id) } else { result = try await appState.testServerConfiguration(name: name, url: url, token: token) } testResult = "Connected — \(result)" } catch { errorMessage = error.localizedDescription } } private func save() { isSaving = true; errorMessage = nil do { if let server { try appState.updateServer(id: server.id, name: name, url: url, token: token.isEmpty ? nil : token) } else { try appState.addServer(name: name, url: url, token: token) } dismiss() } catch { errorMessage = error.localizedDescription; isSaving = false } } }