Chepuhagram/lib/presentation/screens/login_screen.dart

329 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:ui';
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
import 'package:chepuhagram/presentation/screens/account_setup_screen.dart';
import 'package:chepuhagram/presentation/screens/key_recovery_screen.dart';
import 'package:chepuhagram/presentation/screens/qr_login_screen.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../logic/auth_provider.dart';
import '/core/theme_manager.dart';
import 'dart:io'; // Import for File
import 'package:flutter/services.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _totpController = TextEditingController();
bool _showTotpField = false;
String? _errorMessage;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.fastOutSlowIn,
),
);
_animationController.forward();
}
Widget build(BuildContext context) {
final authProvider = context.watch<AuthProvider>();
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Stack(
children: [
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Column(
children: [
Icon(
Icons.messenger_outline,
size: 80,
color: colorScheme.primary,
),
const SizedBox(height: 16),
Text(
"Чепухаграм",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
],
),
),
),
const SizedBox(height: 48),
// Поле Логин
_buildTextField(
controller: _usernameController,
label: "Логин",
icon: Icons.person_outline,
validator: (value) =>
value!.isEmpty ? "Введите логин" : null,
),
const SizedBox(height: 16),
// Поле Пароль
_buildTextField(
controller: _passwordController,
label: "Пароль",
icon: Icons.lock_outline,
obscureText: true,
validator: (value) =>
value!.length < 6 ? "Минимум 6 символов" : null,
),
const SizedBox(height: 16),
// Поле TOTP, если требуется
if (_showTotpField)
_buildTextField(
controller: _totpController,
label: "TOTP код",
icon: Icons.security,
onlyNumbers: true,
validator: (value) =>
value!.isEmpty ? "Введите TOTP код" : null,
),
if (_showTotpField) const SizedBox(height: 16),
// Сообщение об ошибке
if (_errorMessage != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: colorScheme.errorContainer.withOpacity(
0.3,
),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_errorMessage!,
style: TextStyle(
color: colorScheme.onErrorContainer,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
if (_errorMessage != null) const SizedBox(height: 16),
const SizedBox(height: 24),
// Кнопка Входа
ElevatedButton(
onPressed: authProvider.isLoading ? null : _submit,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
child: authProvider.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
"Войти",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const QrLoginScreen(),
),
);
},
icon: const Icon(Icons.qr_code_scanner),
label: const Text(
"Войти по QR-коду",
style: TextStyle(fontWeight: FontWeight.w600),
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
],
),
),
),
),
),
),
],
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required IconData icon,
bool onlyNumbers = false,
bool obscureText = false,
String? Function(String?)? validator,
}) {
final colorScheme = Theme.of(context).colorScheme;
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.35),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
width: 1,
),
),
child: TextFormField(
controller: controller,
obscureText: obscureText,
validator: validator,
keyboardType: onlyNumbers
? TextInputType.number
: TextInputType.text,
inputFormatters: onlyNumbers
? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly]
: null,
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: colorScheme.outline),
prefixIcon: Icon(icon, color: colorScheme.outline),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
),
),
),
),
);
}
void _submit() async {
// Сначала убираем фокус с полей, чтобы клавиатура скрылась
FocusScope.of(context).unfocus();
await Future.delayed(const Duration(milliseconds: 200));
if (!_formKey.currentState!.validate()) return;
try {
final authProvider = context.read<AuthProvider>();
final success = await authProvider.login(
_usernameController.text.trim(),
_passwordController.text.trim(),
totpCode: _showTotpField ? _totpController.text.trim() : null,
);
if (success && mounted) {
await authProvider.initRealtime();
// Определяем путь пользователя после входа
if (authProvider.needsSetup) {
// Путь А: Первичная настройка
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const AccountSetupScreen()),
);
} else if (authProvider.needsKeyRecovery) {
// Путь В: Восстановление ключей
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const KeyRecoveryScreen()),
);
} else {
// Путь Б: Нормальный вход
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
}
} catch (e) {
final error = e.toString().replaceAll('Exception: ', '');
if (mounted) {
setState(() {
_errorMessage = error;
if (error.contains('TOTP код требуется')) {
_showTotpField = true;
}
});
}
}
}
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
_totpController.dispose();
_animationController.dispose();
super.dispose();
}
}