Chepuhagram/lib/presentation/screens/splash_screen.dart

318 lines
11 KiB
Dart
Raw Permalink 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:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:chepuhagram/logic/auth_provider.dart';
import 'package:chepuhagram/logic/contact_provider.dart';
import 'package:chepuhagram/presentation/screens/login_screen.dart';
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/chat_screen.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:chepuhagram/main.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:chepuhagram/domain/services/crypto_service.dart';
import 'package:cryptography/cryptography.dart';
import 'package:flutter/foundation.dart';
import 'package:chepuhagram/core/theme_manager.dart';
import 'dart:ui';
import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with TickerProviderStateMixin {
int? _targetChatId;
String? _statusMessage;
late AnimationController _fadeController;
late AnimationController _pulseController;
late Animation<double> _fadeAnimation;
late Animation<double> _pulseAnimation;
static const String _notificationLaunchKey = 'notification_launch_data';
static const String _contactPublicKey = 'contact_public_key_';
static const String _contactSharedKey = 'contact_shared_key_';
@override
void initState() {
super.initState();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn));
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
);
_pulseController.repeat(reverse: true);
_fadeController.forward();
_setupNotificationHandler();
_initializeApp();
}
void _setupNotificationHandler() {
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
if (message.data['type'] == 'enc_message') {
final senderId = int.tryParse(
message.data['sender_id']?.toString() ?? '',
);
if (senderId != null) {
setState(() => _targetChatId = senderId);
}
}
});
}
Future<void> _initializeApp() async {
if (!mounted) return;
setState(() => _statusMessage = "Подключение...");
final authProvider = context.read<AuthProvider>();
bool? isLoggedIn;
try {
isLoggedIn = await authProvider.tryAutoLogin();
} catch (e) {
setState(
() => _statusMessage =
'Ошибка входа: ${e.toString().replaceAll('Exception: ', '')}',
);
await Future.delayed(const Duration(seconds: 3));
if (mounted) _navigateTo(const LoginScreen());
return;
}
if (!mounted) return;
if (isLoggedIn) {
setState(() => _statusMessage = "Аутентификация...");
bool connected = false;
int connectAttempt = 1;
while (!connected) {
try {
await authProvider.initRealtime();
connected = true;
} catch (e) {
setState(
() => _statusMessage = 'Соединение... (попытка $connectAttempt)',
);
connectAttempt++;
await Future.delayed(const Duration(seconds: 2));
}
}
setState(() => _statusMessage = "Загрузка профиля...");
await authProvider.refreshMe();
if (authProvider.needsSetup) {
_navigateTo(const AccountSetupScreen());
} else if (authProvider.needsKeyRecovery) {
_navigateTo(const KeyRecoveryScreen());
} else {
setState(() => _statusMessage = "Загрузка контактов...");
_loadContactsAndNavigate(authProvider.currentUserId);
}
} else {
_navigateTo(const LoginScreen());
}
}
Future<void> _loadContactsAndNavigate(int? currentUserId) async {
final contactProvider = context.read<ContactProvider>();
final cryptoService = context.read<CryptoService>();
final int? targetId = await _getTargetChatId();
// Мгновенно уходим на экран контактов, показывая лоадеры "Дешифровка..."
_navigateTo(ContactsScreen(targetChatId: targetId));
try {
contactProvider.setCurrentUserId(currentUserId);
await contactProvider.loadContacts(enrichContacts: false);
final storage = const FlutterSecureStorage();
final myPrivKeyBase64 = await cryptoService.getPrivateKey();
if (myPrivKeyBase64 != null) {
final String privKeyFingerprint = myPrivKeyBase64.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '');
final String fingerprint = privKeyFingerprint.substring(
0, privKeyFingerprint.length > 16 ? 16 : privKeyFingerprint.length);
// Проходим по каждому контакту строго ПО ОЧЕРЕДИ
for (var c in contactProvider.contacts) {
final savedKeyHex = await storage.read(
key: '${_contactSharedKey}${fingerprint}_${c.id}',
);
final savedPubKey = await storage.read(
key: '${_contactPublicKey}${fingerprint}_${c.id}',
);
if (savedKeyHex != null && savedPubKey == c.publicKey) {
// Состояние 1: Ключ уже был в локальном хранилище устройства.
// Применяем его моментально без ожидания.
final secretKey = SecretKey(base64Decode(savedKeyHex));
contactProvider.setSharedKey(c.id, secretKey);
CryptoService.cacheSharedKey(c.id, secretKey);
} else if (c.publicKey != null) {
// Состояние 2: Ключа нет на диске. Вычисляем Диффи-Хеллмана для ЭТОГО конкретного контакта.
// Цикл приостановится (await), давая UI продышаться и показать анимацию для предыдущих контактов.
final secretKey = await cryptoService.deriveSharedSecret(
myPrivKeyBase64,
c.publicKey!,
contactId: c.id,
);
// Как только ключ готов — ТУТ ЖЕ обновляем провайдер и RAM-кэш для этой строки.
// Виджет AnimatedSwitcher на экране контактов сразу поймает обновление и красиво проявит текст.
contactProvider.setSharedKey(c.id, secretKey);
CryptoService.cacheSharedKey(c.id, secretKey);
// Извлекаем байты и сохраняем в SecureStorage.
// Опускаем await для записи на диск, чтобы медленная файловая система не тормозила расчет следующего ключа.
secretKey.extractBytes().then((bytes) {
storage.write(
key: '${_contactSharedKey}${fingerprint}_${c.id}',
value: base64Encode(bytes),
);
storage.write(
key: '${_contactPublicKey}${fingerprint}_${c.id}',
value: c.publicKey!,
);
});
}
}
}
} catch (e) {
print("Ошибка при фоновой загрузке контактов или ключей: $e");
}
}
Future<int?> _getTargetChatId() async {
int? targetChatId = _targetChatId;
final prefs = await SharedPreferences.getInstance();
if (targetChatId == null) {
final savedData = prefs.getString(_notificationLaunchKey);
if (savedData != null) {
try {
final data = jsonDecode(savedData) as Map<String, dynamic>;
targetChatId = int.tryParse(data['sender_id']?.toString() ?? '');
} catch (e) {
print('Error parsing saved notification data: $e');
}
}
}
if (targetChatId == null && initialMessage != null) {
if (initialMessage!.data['type'] == 'enc_message') {
targetChatId = int.tryParse(
initialMessage!.data['sender_id']?.toString() ?? '',
);
}
}
await prefs.remove(_notificationLaunchKey);
return targetChatId;
}
void _navigateTo(Widget screen) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => screen),
);
}
}
@override
void dispose() {
_fadeController.dispose();
_pulseController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Stack(
children: [
Center(
child: FadeTransition(
opacity: _fadeAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(),
const Spacer(),
ScaleTransition(
scale: _pulseAnimation,
child: Icon(
Icons.messenger_outline,
size: 80,
color: colorScheme.primary,
),
),
const SizedBox(height: 24),
Text(
"Chepuhagram",
style: TextStyle(
color: colorScheme.primary,
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
const SizedBox(height: 80),
SizedBox(
height: 40,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _statusMessage != null
? Text(
_statusMessage!,
key: ValueKey(_statusMessage),
style: TextStyle(
color: colorScheme.outline,
fontSize: 14,
),
textAlign: TextAlign.center,
)
: const SizedBox.shrink(),
),
),
const CircularProgressIndicator(),
const Spacer(),
Text(
'Made by ArturKarasevich',
style: TextStyle(color: colorScheme.outline, fontSize: 12),
),
const Spacer(),
],
),
),
),
],
),
);
}
}