315 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			315 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						||
//  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
 | 
						||
    private let themeOptions = ThemeOption.ordered
 | 
						||
    
 | 
						||
    @State private var isShowingRegistration = false
 | 
						||
    @State private var showLegacySupportNotice = false
 | 
						||
    @State private var isShowingTerms = false
 | 
						||
    @State private var hasResetTermsOnAppear = 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
 | 
						||
    }
 | 
						||
 | 
						||
    private var isLoginButtonEnabled: Bool {
 | 
						||
        !viewModel.isLoading && isUsernameValid && isPasswordValid && viewModel.hasAcceptedTerms
 | 
						||
    }
 | 
						||
    
 | 
						||
    var body: some View {
 | 
						||
 | 
						||
        ZStack {
 | 
						||
            Color.clear // чтобы поймать тап
 | 
						||
                .contentShape(Rectangle())
 | 
						||
                .onTapGesture {
 | 
						||
                    focusedField = nil
 | 
						||
                }
 | 
						||
 | 
						||
            VStack {
 | 
						||
                HStack {
 | 
						||
 | 
						||
                    Button(action: openLanguageSettings) {
 | 
						||
                        Text("🌍")
 | 
						||
                            .padding()
 | 
						||
                    }
 | 
						||
                    Spacer()
 | 
						||
                    Menu {
 | 
						||
                        ForEach(themeOptions) { option in
 | 
						||
                            Button(action: { selectTheme(option) }) {
 | 
						||
                                themeMenuContent(for: option)
 | 
						||
                                    .opacity(option.isEnabled ? 1.0 : 0.5)
 | 
						||
                            }
 | 
						||
                            .disabled(!option.isEnabled)
 | 
						||
                        }
 | 
						||
                    } label: {
 | 
						||
                        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)
 | 
						||
                }
 | 
						||
 | 
						||
                TermsAgreementCard(
 | 
						||
                    isAccepted: $viewModel.hasAcceptedTerms,
 | 
						||
                    openTerms: {
 | 
						||
                        viewModel.loadTermsIfNeeded()
 | 
						||
                        isShowingTerms = true
 | 
						||
                    }
 | 
						||
                )
 | 
						||
                .padding(.vertical, 12)
 | 
						||
 | 
						||
                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(isLoginButtonEnabled ? Color.blue : Color.gray)
 | 
						||
                            .cornerRadius(8)
 | 
						||
                    }
 | 
						||
                }
 | 
						||
                .disabled(!isLoginButtonEnabled)
 | 
						||
 | 
						||
//                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: "")))
 | 
						||
                )
 | 
						||
            }
 | 
						||
            .onAppear {
 | 
						||
                if !hasResetTermsOnAppear {
 | 
						||
                    viewModel.hasAcceptedTerms = false
 | 
						||
                    hasResetTermsOnAppear = true
 | 
						||
                }
 | 
						||
                if shouldShowLegacySupportNotice {
 | 
						||
                    showLegacySupportNotice = true
 | 
						||
                }
 | 
						||
            }
 | 
						||
            .onTapGesture {
 | 
						||
                focusedField = nil
 | 
						||
            }
 | 
						||
            if showLegacySupportNotice {
 | 
						||
                LegacySupportNoticeView(isPresented: $showLegacySupportNotice)
 | 
						||
                    .transition(.opacity)
 | 
						||
                    .zIndex(1)
 | 
						||
            }
 | 
						||
        }
 | 
						||
        .fullScreenCover(isPresented: $isShowingTerms) {
 | 
						||
            TermsFullScreenView(
 | 
						||
                isPresented: $isShowingTerms,
 | 
						||
                title: NSLocalizedString("Правила сервиса", comment: ""),
 | 
						||
                content: viewModel.termsContent,
 | 
						||
                isLoading: viewModel.isLoadingTerms,
 | 
						||
                errorMessage: viewModel.termsErrorMessage,
 | 
						||
                onRetry: {
 | 
						||
                    viewModel.reloadTerms()
 | 
						||
                }
 | 
						||
            )
 | 
						||
            .onAppear {
 | 
						||
                if viewModel.termsContent.isEmpty {
 | 
						||
                    viewModel.loadTermsIfNeeded()
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
    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 var shouldShowLegacySupportNotice: Bool {
 | 
						||
#if os(iOS)
 | 
						||
        let requiredVersion = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0)
 | 
						||
        return !ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion)
 | 
						||
#else
 | 
						||
        return false
 | 
						||
#endif
 | 
						||
    }
 | 
						||
 | 
						||
    private func openLanguageSettings() {
 | 
						||
        guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
 | 
						||
        UIApplication.shared.open(url)
 | 
						||
    }
 | 
						||
 | 
						||
    private struct LegacySupportNoticeView: View {
 | 
						||
        @Binding var isPresented: Bool
 | 
						||
 | 
						||
        var body: some View {
 | 
						||
            ZStack {
 | 
						||
                Color.black.opacity(0.5)
 | 
						||
                    .ignoresSafeArea()
 | 
						||
                    .onTapGesture {
 | 
						||
                        isPresented = false
 | 
						||
                    }
 | 
						||
 | 
						||
                VStack(spacing: 16) {
 | 
						||
                    Image(systemName: "exclamationmark.triangle.fill")
 | 
						||
                        .font(.system(size: 40, weight: .bold))
 | 
						||
                        .foregroundColor(.yellow)
 | 
						||
 | 
						||
                    Text("Экспериментальная поддержка iOS 15")
 | 
						||
                        .font(.headline)
 | 
						||
                        .multilineTextAlignment(.center)
 | 
						||
 | 
						||
                    Text("Поддержка iOS 15 работает в экспериментальном режиме. Для лучшей совместимости требуется iOS 16+.")
 | 
						||
                        .font(.subheadline)
 | 
						||
                        .foregroundColor(.secondary)
 | 
						||
                        .multilineTextAlignment(.center)
 | 
						||
 | 
						||
                    Button {
 | 
						||
                        isPresented = false
 | 
						||
                    } label: {
 | 
						||
                        Text("Понятно")
 | 
						||
                            .bold()
 | 
						||
                            .frame(maxWidth: .infinity)
 | 
						||
                            .padding()
 | 
						||
                            .background(Color.blue)
 | 
						||
                            .foregroundColor(.white)
 | 
						||
                            .cornerRadius(12)
 | 
						||
                    }
 | 
						||
                }
 | 
						||
                .padding(24)
 | 
						||
                .background(
 | 
						||
                    RoundedRectangle(cornerRadius: 20, style: .continuous)
 | 
						||
                        .fill(Color(.systemBackground))
 | 
						||
                )
 | 
						||
                .frame(maxWidth: 320)
 | 
						||
                .shadow(radius: 10)
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private var selectedThemeOption: ThemeOption {
 | 
						||
        ThemeOption.option(for: themeManager.theme)
 | 
						||
    }
 | 
						||
 | 
						||
    private func themeMenuContent(for option: ThemeOption) -> some View {
 | 
						||
        let isSelected = option == selectedThemeOption
 | 
						||
 | 
						||
        return HStack(spacing: 8) {
 | 
						||
            Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
 | 
						||
                .foregroundColor(isSelected ? .accentColor : .secondary)
 | 
						||
            VStack(alignment: .leading, spacing: 2) {
 | 
						||
                Text(option.title)
 | 
						||
                if let note = option.note {
 | 
						||
                    Text(note)
 | 
						||
                        .font(.caption)
 | 
						||
                        .foregroundColor(.secondary)
 | 
						||
                }
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private func selectTheme(_ option: ThemeOption) {
 | 
						||
        guard let mappedTheme = option.mappedTheme else { return }
 | 
						||
        themeManager.setTheme(mappedTheme)
 | 
						||
    }
 | 
						||
    
 | 
						||
}
 | 
						||
 | 
						||
struct LoginView_Previews: PreviewProvider {
 | 
						||
    static var previews: some View {
 | 
						||
        let viewModel = LoginViewModel()
 | 
						||
        viewModel.isLoading = false // чтобы убрать спиннер
 | 
						||
        return LoginView(viewModel: viewModel)
 | 
						||
    }
 | 
						||
}
 |