Chepuhagram/lib/presentation/screens/login_screen.dart

302 lines
11 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:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../logic/auth_provider.dart';
import '/core/theme_manager.dart';
import 'dart:io'; // Import for File
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 themeProv = context.watch<ThemeProvider>();
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Stack(
children: [
// Background Wallpaper
if (themeProv.wallpaperPath != null)
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: FileImage(File(themeProv.wallpaperPath!)),
fit: BoxFit.cover,
),
),
),
// Blur Overlay
if (themeProv.wallpaperPath != null)
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
color: Colors.black.withOpacity(0.1),
),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
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,
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)),
),
],
),
),
),
),
],
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required IconData icon,
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,
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,
_passwordController.text,
totpCode: _showTotpField ? _totpController.text : 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();
}
}