// // LoginView.swift // VolnahubApp // // Created by cheykrym on 09/06/2025. // import SwiftUI struct LoginView: View { @ObservedObject var viewModel: LoginViewModel @EnvironmentObject private var themeManager: ThemeManager @Environment(\.colorScheme) private var colorScheme @State private var isShowingRegistration = false @FocusState private var focusedField: Field? private enum Field: Hashable { case username case password } private var isUsernameValid: Bool { let pattern = "^[A-Za-z0-9_]{3,32}$" return viewModel.username.range(of: pattern, options: .regularExpression) != nil } private var isPasswordValid: Bool { return viewModel.password.count >= 8 && viewModel.password.count <= 128 } var body: some View { ZStack { Color.clear // чтобы поймать тап .contentShape(Rectangle()) .onTapGesture { focusedField = nil } VStack { HStack { Button(action: openLanguageSettings) { Text("🌍") .padding() } Spacer() Button(action: toggleTheme) { Image(systemName: themeIconName) .padding() } } .onTapGesture { focusedField = nil } Spacer() TextField(NSLocalizedString("Логин", comment: ""), text: $viewModel.username) .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) .disableAutocorrection(true) .focused($focusedField, equals: .username) .onChange(of: viewModel.username) { newValue in if newValue.count > 32 { viewModel.username = String(newValue.prefix(32)) } } // Показываем ошибку для логина if !isUsernameValid && !viewModel.username.isEmpty { Text(NSLocalizedString("Неверный логин", comment: "Неверный логин")) .foregroundColor(.red) .font(.caption) } // Показываем поле пароля SecureField(NSLocalizedString("Пароль", comment: ""), text: $viewModel.password) .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(8) .autocapitalization(.none) .focused($focusedField, equals: .password) .onChange(of: viewModel.password) { newValue in if newValue.count > 32 { viewModel.password = String(newValue.prefix(32)) } } // Показываем ошибку для пароля if !isPasswordValid && !viewModel.password.isEmpty { Text(NSLocalizedString("Неверный пароль", comment: "Неверный пароль")) .foregroundColor(.red) .font(.caption) } var isButtonEnabled: Bool { !viewModel.isLoading && isUsernameValid && isPasswordValid } Button(action: { viewModel.login() }) { if viewModel.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle()) .padding() .frame(maxWidth: .infinity) .background(Color.gray.opacity(0.6)) .cornerRadius(8) } else { Text(NSLocalizedString("Войти", comment: "")) .foregroundColor(.white) .padding() .frame(maxWidth: .infinity) .background(isButtonEnabled ? Color.blue : Color.gray) .cornerRadius(8) } } .disabled(!isButtonEnabled) // Spacer() // Кнопка регистрации Button(action: { isShowingRegistration = true }) { Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация")) .foregroundColor(.blue) } .padding(.top, 10) .sheet(isPresented: $isShowingRegistration) { RegistrationView(viewModel: viewModel, isPresented: $isShowingRegistration) } Spacer() } .padding() .alert(isPresented: $viewModel.showError) { Alert( title: Text(NSLocalizedString("Ошибка авторизации", comment: "")), message: Text(viewModel.errorMessage), dismissButton: .default(Text(NSLocalizedString("OK", comment: ""))) ) } .onTapGesture { focusedField = nil } } } private var themeIconName: String { switch themeManager.theme { case .system: return colorScheme == .dark ? "moon.fill" : "sun.max.fill" case .light: return "sun.max.fill" case .oledDark: return "moon.fill" } } private func toggleTheme() { themeManager.toggleTheme(from: colorScheme) } private func openLanguageSettings() { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) } } struct LoginView_Previews: PreviewProvider { static var previews: some View { let viewModel = LoginViewModel() viewModel.isLoading = false // чтобы убрать спиннер return LoginView(viewModel: viewModel) } }