From f9026ebf8753315118372f670c4b43a7f4529e9e Mon Sep 17 00:00:00 2001 From: cheykrym Date: Wed, 3 Dec 2025 07:05:41 +0300 Subject: [PATCH] selector --- yobble/Resources/Localizable.xcstrings | 21 +++ yobble/Views/Login/LoginView.swift | 182 +++++++++++++++++++++++-- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index dc2c030..4165506 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -753,6 +753,9 @@ } } } + }, + "Какой режим попробовать?" : { + }, "Кастомная" : { "localizations" : { @@ -1509,6 +1512,9 @@ }, "Основной режим находится в ранней разработке (около 10%)." : { + }, + "Оставить мессенджер" : { + }, "Отключить" : { "comment" : "Кнопка подтверждения отключения 2FA" @@ -1749,6 +1755,9 @@ }, "Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : { "comment" : "FAQ answer: reset password" + }, + "По умолчанию включён полный интерфейс. Мессенджер оставляет только общение и готов к работе уже сейчас. Можно переключиться в любой момент." : { + }, "Повторите пароль" : { "comment" : "Поле подтверждения пароля на приложение" @@ -1831,6 +1840,9 @@ } } } + }, + "Позже" : { + }, "Поиск" : { @@ -1880,6 +1892,9 @@ } } } + }, + "Полный интерфейс" : { + }, "Получать коды на email при входе" : { "comment" : "Переключатель отправки кодов при входе" @@ -2268,6 +2283,9 @@ }, "Режим мессенжера" : { + }, + "Рекомендуем новичкам" : { + }, "Сборка:" : { "localizations" : { @@ -2734,6 +2752,9 @@ }, "Экспериментальная поддержка iOS 15" : { + }, + "Экспериментальный режим" : { + }, "Это устройство" : { "comment" : "Заголовок секции текущего устройства" diff --git a/yobble/Views/Login/LoginView.swift b/yobble/Views/Login/LoginView.swift index 41251dc..dbc5c78 100644 --- a/yobble/Views/Login/LoginView.swift +++ b/yobble/Views/Login/LoginView.swift @@ -9,22 +9,64 @@ import SwiftUI struct LoginView: View { @ObservedObject var viewModel: LoginViewModel + @AppStorage("messengerModeEnabled") private var isMessengerModeEnabled: Bool = false + @State private var isShowingMessengerPrompt: Bool = true + @State private var pendingMessengerMode: Bool = UserDefaults.standard.bool(forKey: "messengerModeEnabled") var body: some View { + ZStack { + content + .animation(.easeInOut(duration: 0.25), value: viewModel.loginFlowStep) + .allowsHitTesting(!isShowingMessengerPrompt) + .blur(radius: isShowingMessengerPrompt ? 3 : 0) + + if isShowingMessengerPrompt { + Color.black.opacity(0.35) + .ignoresSafeArea() + .transition(.opacity) + + MessengerModePrompt( + selection: $pendingMessengerMode, + onAccept: applyMessengerModeSelection, + onSkip: dismissMessengerPrompt + ) + .padding(.horizontal, 24) + .transition(.scale.combined(with: .opacity)) + } + } + .onAppear { + pendingMessengerMode = isMessengerModeEnabled + withAnimation { + isShowingMessengerPrompt = true + } + } + } + + private var content: some View { ZStack { switch viewModel.loginFlowStep { case .passwordlessRequest: - PasswordlessRequestView(viewModel: viewModel) + PasswordlessRequestView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt) .transition(.move(edge: .trailing).combined(with: .opacity)) case .passwordlessVerify: - PasswordlessVerifyView(viewModel: viewModel) + PasswordlessVerifyView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt) .transition(.move(edge: .leading).combined(with: .opacity)) case .password: PasswordLoginView(viewModel: viewModel) .transition(.opacity) } } - .animation(.easeInOut(duration: 0.25), value: viewModel.loginFlowStep) + } + + private func applyMessengerModeSelection() { + isMessengerModeEnabled = pendingMessengerMode + dismissMessengerPrompt() + } + + private func dismissMessengerPrompt() { + withAnimation { + isShowingMessengerPrompt = false + } } } @@ -352,6 +394,7 @@ struct PasswordLoginView: View { private struct PasswordlessRequestView: View { @ObservedObject var viewModel: LoginViewModel + let shouldAutofocus: Bool @FocusState private var isFieldFocused: Bool var body: some View { @@ -432,9 +475,12 @@ private struct PasswordlessRequestView: View { .onTapGesture { isFieldFocused = false } - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - isFieldFocused = true + .onAppear(perform: scheduleFocusIfNeeded) + .onChange(of: shouldAutofocus) { newValue in + if newValue { + scheduleFocusIfNeeded() + } else { + isFieldFocused = false } } .loginErrorAlert(viewModel: viewModel) @@ -444,10 +490,20 @@ private struct PasswordlessRequestView: View { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) } + + private func scheduleFocusIfNeeded() { + guard shouldAutofocus else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + if shouldAutofocus { + isFieldFocused = true + } + } + } } private struct PasswordlessVerifyView: View { @ObservedObject var viewModel: LoginViewModel + let shouldAutofocus: Bool @FocusState private var isCodeFieldFocused: Bool var body: some View { @@ -458,8 +514,8 @@ private struct PasswordlessVerifyView: View { VStack(alignment: .leading, spacing: 8) { Text(NSLocalizedString("Введите код", comment: "")) .font(.largeTitle).bold() -// Text(String(format: NSLocalizedString("Мы отправили код на %@", comment: ""), viewModel.passwordlessLogin)) -// .foregroundColor(.secondary) + Text(String(format: NSLocalizedString("Мы отправили код на %@", comment: ""), viewModel.passwordlessLogin)) + .foregroundColor(.secondary) } OTPInputView(code: $viewModel.verificationCode, isFocused: $isCodeFieldFocused) @@ -536,9 +592,12 @@ private struct PasswordlessVerifyView: View { .onTapGesture { isCodeFieldFocused = true } - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - isCodeFieldFocused = true + .onAppear(perform: scheduleFocusIfNeeded) + .onChange(of: shouldAutofocus) { newValue in + if newValue { + scheduleFocusIfNeeded() + } else { + isCodeFieldFocused = false } } .loginErrorAlert(viewModel: viewModel) @@ -548,6 +607,15 @@ private struct PasswordlessVerifyView: View { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) } + + private func scheduleFocusIfNeeded() { + guard shouldAutofocus else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + if shouldAutofocus { + isCodeFieldFocused = true + } + } + } } private struct OTPInputView: View { @@ -673,6 +741,98 @@ private struct LoginTopBar: View { } } +private struct MessengerModePrompt: View { + @Binding var selection: Bool + let onAccept: () -> Void + let onSkip: () -> Void + + var body: some View { + VStack(spacing: 20) { + Text(NSLocalizedString("Какой режим попробовать?", comment: "")) + .font(.title3.bold()) + .multilineTextAlignment(.center) + + Text(NSLocalizedString("По умолчанию включён полный интерфейс. Мессенджер оставляет только общение и готов к работе уже сейчас. Можно переключиться в любой момент.", comment: "")) + .font(.callout) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + VStack(spacing: 12) { + optionButton( + title: NSLocalizedString("Оставить мессенджер", comment: ""), + subtitle: NSLocalizedString("Рекомендуем новичкам", comment: ""), + isMessenger: true + ) + + optionButton( + title: NSLocalizedString("Полный интерфейс", comment: ""), + subtitle: NSLocalizedString("Экспериментальный режим", comment: ""), + isMessenger: false + ) + } + + HStack(spacing: 12) { + Button(action: onSkip) { + Text(NSLocalizedString("Позже", comment: "")) + .font(.callout) + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(Color.secondary.opacity(0.3)) + ) + } + + Button(action: onAccept) { + Text(NSLocalizedString("Применить", comment: "")) + .font(.callout.bold()) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.accentColor) + ) + } + } + } + .padding(24) + .background( + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill(Color(.systemBackground)) + ) + .shadow(color: Color.black.opacity(0.2), radius: 30, x: 0, y: 12) + } + + private func optionButton(title: String, subtitle: String, isMessenger: Bool) -> some View { + Button { + selection = isMessenger + } label: { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(title) + .font(.headline) + Spacer() + if selection == isMessenger { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + } + } + Text(subtitle) + .font(.footnote) + .foregroundColor(.secondary) + } + .padding() + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(selection == isMessenger ? Color.accentColor.opacity(0.15) : Color(.secondarySystemBackground)) + ) + } + .buttonStyle(.plain) + } +} + private extension View { func loginErrorAlert(viewModel: LoginViewModel) -> some View { alert(isPresented: Binding(