import SwiftUI struct LoginView: View { @Environment(AppState.self) private var app @State private var serverURL: String = "" @State private var username: String = "" @State private var password: String = "" @State private var remember: Bool = true @State private var isLoading: Bool = false var body: some View { #if os(iOS) iOSBody #else macOSBody #endif } // MARK: - iOS #if os(iOS) private var iOSBody: some View { VStack(spacing: 0) { // Header with green gradient background ZStack { LinearGradient( colors: [Color.accentColor.opacity(0.85), Color.accentColor.opacity(0.55)], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea(edges: .top) VStack(spacing: 10) { Image(systemName: "books.vertical.fill") .font(.system(size: 52, weight: .regular)) .foregroundStyle(.white) Text("ABS Client") .font(.title.bold()) .foregroundStyle(.white) Text("Verbinde dich mit deinem Audiobookshelf-Server") .font(.subheadline) .foregroundStyle(.white.opacity(0.85)) .multilineTextAlignment(.center) } .padding(.top, 56) .padding(.bottom, 32) .padding(.horizontal, 24) } .fixedSize(horizontal: false, vertical: true) // Form fields — uses native iOS Form appearance Form { Section { TextField("https://abs.example.com", text: $serverURL) .textContentType(.URL) .keyboardType(.URL) .textInputAutocapitalization(.never) .autocorrectionDisabled(true) .submitLabel(.next) } header: { Text("Server-URL") } Section { TextField("Benutzername", text: $username) .textContentType(.username) .textInputAutocapitalization(.never) .autocorrectionDisabled(true) .submitLabel(.next) SecureField("Passwort", text: $password) .textContentType(.password) .submitLabel(.go) .onSubmit { if canLogin { doLogin() } } } header: { Text("Anmeldedaten") } Section { Toggle("Anmeldung merken", isOn: $remember) } if let err = app.auth.errorMessage { Section { Label(err, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.red) .font(.callout) } } Section { Button(action: doLogin) { HStack { Spacer() if isLoading { ProgressView() } else { Text("Einloggen").bold() } Spacer() } } .disabled(!canLogin) } } .scrollContentBackground(.hidden) .background(Color(.systemGroupedBackground)) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemGroupedBackground).ignoresSafeArea()) } private var canLogin: Bool { !isLoading && !serverURL.isEmpty && !username.isEmpty && !password.isEmpty } #endif // MARK: - macOS #if os(macOS) private var macOSBody: some View { VStack(spacing: 16) { Spacer() Image(systemName: "books.vertical.fill") .font(.system(size: 48)) .foregroundStyle(.tint) Text("ABS Client") .font(.largeTitle).bold() Text("Verbinde dich mit deinem Audiobookshelf-Server") .foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 12) { labeledField(label: "Server-URL") { TextField("", text: $serverURL) .textFieldStyle(.roundedBorder) .disableAutocorrection(true) } labeledField(label: "Benutzername") { TextField("", text: $username) .textFieldStyle(.roundedBorder) .disableAutocorrection(true) } labeledField(label: "Passwort") { SecureField("", text: $password) .textFieldStyle(.roundedBorder) } Toggle("Anmeldung merken", isOn: $remember) .toggleStyle(.checkbox) } .frame(maxWidth: 380) if let err = app.auth.errorMessage { Text(err) .foregroundStyle(.red) .font(.callout) .multilineTextAlignment(.center) .frame(maxWidth: 380) } Button(action: doLogin) { if isLoading { ProgressView().controlSize(.small) } else { Text("Einloggen").frame(maxWidth: 200) } } .buttonStyle(.borderedProminent) .controlSize(.large) .disabled(isLoading || serverURL.isEmpty || username.isEmpty || password.isEmpty) .keyboardShortcut(.defaultAction) Spacer() } .padding(32) } @ViewBuilder private func labeledField(label: String, @ViewBuilder content: () -> C) -> some View { VStack(alignment: .leading, spacing: 4) { Text(label).font(.subheadline).foregroundStyle(.secondary) content() } } #endif // MARK: - Shared private func doLogin() { isLoading = true Task { await app.auth.login( serverURL: serverURL, username: username, password: password, remember: remember ) isLoading = false } } }