318 lines
11 KiB
Dart
318 lines
11 KiB
Dart
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(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|