import 'dart:convert'; import 'package:chepuhagram/core/constants.dart'; import 'package:chepuhagram/domain/services/aPI_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../widgets/contact_tile.dart'; import '../screens/settings_screen.dart'; import '../screens/new_chat_screen.dart'; import '../screens/chat_screen.dart'; import '/logic/contact_provider.dart'; import '/logic/auth_provider.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:chepuhagram/main.dart'; import 'dart:async'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:open_filex/open_filex.dart'; class ContactsScreen extends StatefulWidget { final int? targetChatId; const ContactsScreen({super.key, this.targetChatId}); @override State createState() => _ContactsScreenState(); } class _ContactsScreenState extends State with RouteAware { static const String _notificationLaunchKey = 'notification_launch_data'; StreamSubscription? _socketSubscription; bool _isDownloading = false; double _downloadProgress = 0.0; CancelToken? _cancelToken = CancelToken(); String? _latestApkUrl; bool _showUpdateBanner = false; @override void initState() { super.initState(); print('ContactsScreen initState, targetChatId: ${widget.targetChatId}'); _setupPushNotifications(); WidgetsBinding.instance.addPostFrameCallback((_) { final authProvider = context.read(); final contactProvider = context.read(); // Установить текущего пользователя и загрузить контакты с сообщениями print( 'Setting current user ID in ContactProvider: ${authProvider.currentUserId}', ); contactProvider.setCurrentUserId(authProvider.currentUserId); _initContacts(); }); WidgetsBinding.instance.addPostFrameCallback((_) { _checkAppUpdate(); }); } Future _initContacts() async { final contactProvider = context.read(); // Ждем завершения загрузки контактов await contactProvider.loadContacts(); print('Contacts loaded, checking targetChatId: ${widget.targetChatId}'); // Дальнейшая логика выполнится только после того, как loadContacts завершится if (widget.targetChatId != null) { _navigateToTargetChat(); } else { _checkSavedNotificationTarget(); } } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute); } @override void didPopNext() { print("Пользователь вернулся на этот экран!"); _refreshData(); } @override void dispose() { routeObserver.unsubscribe(this); _socketSubscription?.cancel(); super.dispose(); } void _refreshData() { print("Обновляем данные контактов и сообщений..."); final contactProvider = context.read(); contactProvider.loadContacts(); } Future _checkSavedNotificationTarget() async { final prefs = await SharedPreferences.getInstance(); final savedData = prefs.getString(_notificationLaunchKey); if (savedData == null) { print('No saved notification data found in SharedPreferences'); return; } try { final data = jsonDecode(savedData) as Map; print('Recovered saved notification data: $data'); final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); final type = data['type']?.toString(); // Поддерживаем старый payload (только sender_id) и новый (type+sender_id) if (senderId != null && (type == null || type == 'enc_message')) { print('Recovered targetChatId from saved data: $senderId'); await prefs.remove(_notificationLaunchKey); _navigateToTargetChatWithId(senderId); return; } print('Saved notification data is not a valid payload: $data'); await prefs.remove(_notificationLaunchKey); } catch (e) { print('Error parsing saved notification data: $e'); await prefs.remove(_notificationLaunchKey); } } void _navigateToTargetChat() { if (widget.targetChatId == null) return; _navigateToTargetChatWithId(widget.targetChatId!); } void _navigateToTargetChatWithId(int targetChatId) async { print('_navigateToTargetChat called with targetChatId: $targetChatId'); final contactProvider = context.read(); try { final contact = contactProvider.contacts.firstWhere( (c) => c.id == targetChatId, ); print('Auto-navigating to chat with contact: ${contact.username}'); currentActiveChatContactId = targetChatId; // Устанавливаем активный чат final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)), ); if (result != null) { _refreshData(); // Обновляем данные при возвращении с чата, если нужно } } catch (e) { print('Target contact with id $targetChatId not found: $e'); } } Future _checkAppUpdate() async { print('Проверка обновлений'); PackageInfo packageInfo = await PackageInfo.fromPlatform(); try { // 1. Запрос к вашему FastAPI final response = await http.get( Uri.parse('${AppConstants.baseUrl}/check-update'), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); final String latestVersion = data['latest_version']; print('444444'); print(latestVersion); print(packageInfo.version); // Сравнение версий (предположим, у вас есть способ получить текущую версию) if (latestVersion != packageInfo.version) { setState(() { _showUpdateBanner = true; _latestApkUrl = data['apk_url']; }); } } } catch (e) { print("Ошибка проверки обновлений: $e"); } } Future _setupPushNotifications() async { // Request permissions await FirebaseMessaging.instance.requestPermission(); String? token = await FirebaseMessaging.instance.getToken(); if (token != null) { ApiService apiService = ApiService(); print(token); await apiService.updateFcmToken(token); } // Listen for token refresh FirebaseMessaging.instance.onTokenRefresh.listen((newToken) { ApiService apiService = ApiService(); apiService.updateFcmToken(newToken); print('FCM Token refreshed: $newToken'); }); // Listen for foreground messages FirebaseMessaging.onMessage.listen((RemoteMessage message) { print('Foreground message received: ${message.data}'); if (message.data['type'] == 'enc_message') { _handleIncomingMessage(message); } }); // Handle notification tap when app was terminated/backgrounded FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { print('Notification tapped, app opened: ${message.data}'); if (message.data['type'] == 'enc_message') { final senderId = int.tryParse( message.data['sender_id']?.toString() ?? '', ); if (senderId != null) { _navigateToChatFromNotification(senderId); } else { print( 'Notification tap contains invalid sender_id: ${message.data['sender_id']}', ); } } }); } void _navigateToChatFromNotification(int senderId) async { final contactProvider = context.read(); print('Navigate to chat from notification with senderId: $senderId'); // Если контакты еще не загружены, ждем их загрузки if (contactProvider.contacts.isEmpty) { print('Contacts not loaded yet, waiting...'); // Ждем немного и пробуем снова Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { _navigateToChatFromNotification(senderId); } }); return; } try { final contact = contactProvider.contacts.firstWhere( (c) => c.id == senderId, ); print('Navigating to chat from notification: ${contact.username}'); currentActiveChatContactId = senderId; // Устанавливаем активный чат final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)), ); if (result != null) { _refreshData(); // Обновляем данные при возвращении с чата, если нужно } } catch (e) { // Contact not found, stay on contacts screen print('Contact not found for notification: $senderId'); } } Future _handleIncomingMessage(RemoteMessage message) async { try { // Проверяем, не находимся ли мы уже в чате с отправителем final senderId = int.tryParse( message.data['sender_id']?.toString() ?? '', ); if (senderId != null && currentActiveChatContactId == senderId) { print('Already in chat with sender $senderId, skipping notification'); return; } // Ensure notification channel exists const AndroidNotificationChannel channel = AndroidNotificationChannel( 'chat_id', 'Messages', description: 'Chat messages notifications', importance: Importance.high, ); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >() ?.createNotificationChannel(channel); final crypto = CryptoService(); final myPrivKey = await crypto.getPrivateKey(); if (myPrivKey == null) { print('Private key not found, cannot decrypt message'); return; } final sharedSecret = await crypto.deriveSharedSecret( myPrivKey, message.data['public_key'], ); final decryptedText = await crypto.decryptMessage( message.data['content'], sharedSecret, ); // Show local notification await flutterLocalNotificationsPlugin.show( message.hashCode, message.data['username'] ?? 'Unknown', decryptedText, const NotificationDetails( android: AndroidNotificationDetails('chat_id', 'Messages'), ), payload: jsonEncode({ 'type': 'enc_message', 'sender_id': message.data['sender_id'], 'timestamp': message.data['timestamp'] ?? DateTime.now().toIso8601String(), }), ); if (message.data['type'] == 'enc_message') { final contactProvider = context.read(); contactProvider.loadContacts(); } } catch (e) { print('Error processing foreground message: $e'); } } @override Widget build(BuildContext context) { final double fabBottomPadding = _showUpdateBanner ? 120.0 : 16.0; return Scaffold( appBar: AppBar( title: Text( "Chepuhagram", style: TextStyle(fontWeight: FontWeight.bold), ), centerTitle: false, elevation: 0, actions: [IconButton(icon: const Icon(Icons.search), onPressed: () {})], ), body: Stack( children: [ Consumer( builder: (context, contactProvider, child) { if (contactProvider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (contactProvider.error != null) { return Center(child: Text('Error: ${contactProvider.error}')); } return ListView.separated( itemCount: contactProvider.contacts.length, separatorBuilder: (context, index) => Divider( height: 1, indent: 80, color: Theme.of(context).colorScheme.primaryContainer, ), itemBuilder: (context, index) { final contact = contactProvider.contacts[index]; return ContactTile( contact: contact, onTap: () async { final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => ChatScreen(contact: contact), ), ); if (result != null) { _refreshData(); // Обновляем данные при возвращении с чата, если нужно } }, ); }, ); }, ), if (_showUpdateBanner) Positioned( left: 0, right: 0, bottom: 40, child: _buildUpdateBanner(), ), ], ), floatingActionButton: AnimatedPadding( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, padding: EdgeInsets.only(bottom: fabBottomPadding), child: FloatingActionButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const NewChatScreen()), ); }, child: const Icon(Icons.edit), ), ), drawer: Drawer( child: ListView( padding: EdgeInsets.zero, children: [ // Шапка меню с данными юзера Consumer( builder: (context, authProvider, _) { final username = authProvider.username; final displayName = authProvider.displayName; final initials = (displayName.isNotEmpty ? displayName : (username ?? 'U')) .trim() .split(RegExp(r'\s+')) .where((p) => p.isNotEmpty) .take(2) .map((p) => p[0].toUpperCase()) .join(); return UserAccountsDrawerHeader( accountName: Text( displayName, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), ), accountEmail: Text( username == null || username.isEmpty ? '' : '@$username', style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), ), currentAccountPicture: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.onSurface, child: Text( initials.isEmpty ? 'U' : initials, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primaryContainer, ), ), ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.inversePrimary, ), ); }, ), ListTile( leading: const Icon(Icons.settings), title: const Text("Настройки"), onTap: () { // Закрываем Drawer и переходим на экран настроек Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => SettingsScreen()), ); }, ), ListTile( leading: const Icon(Icons.info_outline), title: const Text("О приложении"), onTap: () { /* ... */ }, ), ], ), ), ); } Future _startDownload() async { if (_latestApkUrl == null) return; // Показываем индикатор setState(() => _isDownloading = true); final dir = await getExternalStorageDirectory(); final path = '${dir!.path}/update.apk'; final file = File(path); // Удаляем старый файл, если он есть, чтобы гарантировать чистоту if (await file.exists()) { await file.delete(); } try { // Скачиваем файл «в лоб» await Dio().download( _latestApkUrl!, path, cancelToken: _cancelToken, onReceiveProgress: (rec, total) { if (total != -1) { if (mounted) { setState(() => _downloadProgress = rec / total); } } }, ); // После успешного скачивания — установка final result = await OpenFilex.open(path); if (result.type != ResultType.done) { print("Ошибка при установке: ${result.message}"); } } on DioException catch (e) { if (e.type != DioExceptionType.cancel) { print("Ошибка скачивания: $e"); } } catch (e) { print("Ошибка: $e"); } finally { if (mounted) { setState(() => _isDownloading = false); } } } void _cancelDownload() { _cancelToken?.cancel("Отменено"); setState(() { _isDownloading = false; _downloadProgress = 0.0; }); } Widget _buildUpdateBanner() { return Container( margin: const EdgeInsets.fromLTRB( 12, 0, 12, 16, ), // Отступы от краев и снизу child: Material( elevation: 6, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.orange.shade600, Colors.deepOrange.shade400], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Icon( Icons.system_update_alt, color: Colors.white, size: 28, ), const SizedBox(width: 12), Expanded( child: Text( _isDownloading ? 'Скачивание ${(_downloadProgress * 100).toStringAsFixed(0)}%' : "Доступно новое обновление!", style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), ), ), TextButton( onPressed: () async { if (_isDownloading) { // Если уже качаем — отменяем _cancelToken?.cancel("Пользователь отменил загрузку"); setState(() { _isDownloading = false; _cancelToken = null; // Обязательно обнуляем токен! _downloadProgress = 0.0; }); } else { // Если не качаем — запускаем setState(() { _isDownloading = true; _cancelToken = CancelToken(); // Создаем новый токен перед началом }); // ВАЖНО: вызываем саму функцию скачивания await _startDownload(); } }, style: TextButton.styleFrom( backgroundColor: Colors.white24, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( _isDownloading ? "Отмена" : "Обновить", style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ], ), if (_isDownloading) ...[ const SizedBox(height: 12), LinearProgressIndicator( value: _downloadProgress, color: Colors.white, backgroundColor: Colors.white24, ), ], ], ), ), ), ); } }