import SwiftUI struct SetupView: View { @Environment(WebDAVSettings.self) private var settings @Environment(\.dismiss) private var dismiss @State private var url = "" @State private var username = "" @State private var password = "" @State private var notesPath = "/notes/" @State private var testing = false @State private var testResult: String? @State private var testOK = false @State private var themeManager = ThemeManager.shared let isInitial: Bool var body: some View { NavigationStack { Form { Section("Server") { TextField("URL", text: $url, prompt: Text("https://webdav.example.com")) .textContentType(.URL) .textInputAutocapitalization(.never) .autocorrectionDisabled() .keyboardType(.URL) TextField("Notes path", text: $notesPath) .textInputAutocapitalization(.never) .autocorrectionDisabled() } Section("Authentication") { TextField("Username", text: $username) .textInputAutocapitalization(.never) .autocorrectionDisabled() SecureField("Password", text: $password) .textContentType(.password) } Section { Button { Task { await testConnection() } } label: { HStack { Text("Test connection") Spacer() if testing { ProgressView() } } } .disabled(testing || !canTest) if let r = testResult { Label(r, systemImage: testOK ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundStyle(testOK ? .green : .red) .font(.footnote) } } Section { Text("The password is stored securely in the iOS Keychain.") .font(.footnote) .foregroundStyle(.secondary) } appearanceSection } .scrollContentBackground(.hidden) .background(themeManager.current.background) .navigationTitle(isInitial ? "Connect" : "Settings") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(themeManager.current.secondaryBackground, for: .navigationBar) .toolbar { if !isInitial { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } ToolbarItem(placement: .confirmationAction) { Button("Save") { save() } .disabled(!canSave) } } .onAppear(perform: load) } .preferredColorScheme(themeManager.current.colorScheme) .tint(themeManager.current.accent) } private var canTest: Bool { !url.trimmingCharacters(in: .whitespaces).isEmpty && !username.trimmingCharacters(in: .whitespaces).isEmpty } private var canSave: Bool { canTest && !password.trimmingCharacters(in: .whitespaces).isEmpty } private func load() { url = settings.url username = settings.username password = settings.password notesPath = settings.notesPath } private func save() { settings.url = url.trimmingCharacters(in: .whitespaces) settings.username = username.trimmingCharacters(in: .whitespaces) settings.password = password.trimmingCharacters(in: .whitespaces) settings.notesPath = notesPath.trimmingCharacters(in: .whitespaces) dismiss() } private var appearanceSection: some View { Section { ForEach(AppTheme.all) { theme in HStack(spacing: 12) { themeSwatches(theme) Text(theme.name) Spacer() if themeManager.current.id == theme.id { Image(systemName: "checkmark") .foregroundStyle(Color.accentColor) .fontWeight(.semibold) } } .contentShape(Rectangle()) .onTapGesture { themeManager.select(theme) } } } header: { Text("Appearance") } } private func themeSwatches(_ theme: AppTheme) -> some View { HStack(spacing: 2) { RoundedRectangle(cornerRadius: 3) .fill(theme.background) .frame(width: 18, height: 28) .overlay(RoundedRectangle(cornerRadius: 3).strokeBorder(Color.secondary.opacity(0.2), lineWidth: 0.5)) RoundedRectangle(cornerRadius: 3) .fill(theme.secondaryBackground) .frame(width: 18, height: 28) RoundedRectangle(cornerRadius: 3) .fill(theme.accent) .frame(width: 18, height: 28) } .clipShape(RoundedRectangle(cornerRadius: 4)) .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(Color.secondary.opacity(0.15), lineWidth: 0.5)) } @MainActor private func testConnection() async { testing = true defer { testing = false } testResult = nil let probe = WebDAVConfig( url: url.trimmingCharacters(in: .whitespaces), username: username.trimmingCharacters(in: .whitespaces), password: password.trimmingCharacters(in: .whitespaces), notesPath: notesPath.trimmingCharacters(in: .whitespaces) ) do { let count = try await WebDAVClient.probe(config: probe) testOK = true testResult = "Connected. \(count) note\(count == 1 ? "" : "s") found." } catch URLError.userAuthenticationRequired { testOK = false testResult = "Invalid credentials (401 Unauthorized)." } catch { testOK = false testResult = error.localizedDescription } } } #Preview { SetupView(isInitial: true) .environment(WebDAVSettings()) }