296 lines
9.9 KiB
Dart
296 lines
9.9 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';
|
|
|
|
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 {
|
|
// Navigate to ContactsScreen while contacts are loading in the background
|
|
_navigateTo(ContactsScreen(targetChatId: await _getTargetChatId()));
|
|
|
|
try {
|
|
final contactProvider = context.read<ContactProvider>();
|
|
contactProvider.setCurrentUserId(currentUserId);
|
|
await contactProvider.loadContacts(enrichContacts: false);
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final myPrivKeyBase64 = await context.read<CryptoService>().getPrivateKey();
|
|
|
|
if (myPrivKeyBase64 != null) {
|
|
final Map<int, String> keysToCompute = {};
|
|
for (var c in contactProvider.contacts) {
|
|
final savedKeyHex = prefs.getString('$_contactSharedKey${c.id}');
|
|
final savedPubKey = prefs.getString('$_contactPublicKey${c.id}');
|
|
if (savedKeyHex != null && savedPubKey == c.publicKey) {
|
|
contactProvider.setSharedKey(c.id, SecretKey(base64Decode(savedKeyHex)));
|
|
} else if (c.publicKey != null) {
|
|
keysToCompute[c.id] = c.publicKey!;
|
|
}
|
|
}
|
|
|
|
final computedKeys = await compute(
|
|
CryptoService.computeSharedKeysTask,
|
|
{'keysMap': keysToCompute, 'privKey': myPrivKeyBase64},
|
|
);
|
|
|
|
computedKeys.forEach((id, bytes) {
|
|
contactProvider.setSharedKey(id, SecretKey(bytes));
|
|
prefs.setString('$_contactSharedKey$id', base64Encode(bytes));
|
|
prefs.setString('$_contactPublicKey$id', keysToCompute[id]!);
|
|
});
|
|
}
|
|
} 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 themeProv = context.watch<ThemeProvider>();
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
if (themeProv.wallpaperPath != null)
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: FileImage(File(themeProv.wallpaperPath!)),
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
if (themeProv.wallpaperPath != null)
|
|
BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
|
child: Container(color: Colors.black.withOpacity(0.1)),
|
|
),
|
|
|
|
Center(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
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 SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|