diff --git a/Forji/Forji/Views/InstanceFormView.swift b/Forji/Forji/Views/InstanceFormView.swift index 8a72c9e..087c7bf 100644 --- a/Forji/Forji/Views/InstanceFormView.swift +++ b/Forji/Forji/Views/InstanceFormView.swift @@ -2,6 +2,7 @@ import ForgejoKit import SwiftData import SwiftUI +// swiftlint:disable file_length // swiftlint:disable:next type_body_length struct InstanceFormView: View { enum Mode: Identifiable { @@ -23,6 +24,7 @@ struct InstanceFormView: View { @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss + @Environment(\.accessibilityReduceMotion) private var reduceMotion @Query(sort: \ForgejoInstance.lastUsed, order: .reverse) private var instances: [ForgejoInstance] @State private var authService: AuthenticationService @@ -55,6 +57,14 @@ struct InstanceFormView: View { self.mode = mode } + /// "Login"/"Save" at rest; the present-progressive form while the request runs. + private var buttonTitle: String { + if isLoading { + return isAddMode ? "Signing in…" : "Saving…" + } + return isAddMode ? "Login" : "Save" + } + var body: some View { NavigationStack { Form { @@ -144,21 +154,27 @@ struct InstanceFormView: View { Section { Button(action: handleSave) { - HStack { - Spacer() + // A leading spinner with a status label keeps the button + // full while signing in, instead of emptying it to a lone + // spinner. Reduce Motion drops the morph. + HStack(spacing: 8) { if isLoading { ProgressView() - .progressViewStyle(.circular) - } else { - Text(isAddMode ? "Login" : "Save") - .fontWeight(.semibold) + .controlSize(.regular) + .tint(.white) + .transition(.blurReplace) } - Spacer() + Text(buttonTitle) + .fontWeight(.semibold) + .contentTransition(.opacity) } + .frame(maxWidth: .infinity) + .animation(reduceMotion ? nil : .smooth(duration: 0.3), value: isLoading) } .buttonStyle(.borderedProminent) + .controlSize(.large) .disabled( - isLoading || serverURL.isEmpty + serverURL.isEmpty || (authMode == .credentials && (username.isEmpty || (isAddMode && password.isEmpty) || (needsOTP && otpCode.isEmpty))) @@ -242,6 +258,9 @@ struct InstanceFormView: View { // swiftlint:disable:next function_body_length cyclomatic_complexity private func handleSave() { + // The button keeps its prominent look while loading; guard re-entry here. + guard !isLoading else { return } + if !name.isEmpty { let conflict = instances.first { inst in let instId = inst.serverURL + inst.username