From fb96dd09960391deee10e456a70322156788b0ab Mon Sep 17 00:00:00 2001 From: needle10 Date: Sun, 16 Nov 2025 22:47:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B2=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B5,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=BF=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D1=81=D0=B8=20=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=B2=D1=85=D0=BE=D0=B4=D0=B0,=20=D0=B2=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=B0=D0=BA=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20socks5=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api_service.dart | 18 +- lib/chats_screen.dart | 3 +- lib/password_management_screen.dart | 8 - lib/phone_entry_screen.dart | 105 +++++++++ lib/proxy_service.dart | 93 +++++--- .../settings/proxy_settings_screen.dart | 11 +- lib/widgets/message_preview_dialog.dart | 206 ++++++++++++++++++ 7 files changed, 392 insertions(+), 52 deletions(-) diff --git a/lib/api_service.dart b/lib/api_service.dart index fb249cf..6e1ab5a 100644 --- a/lib/api_service.dart +++ b/lib/api_service.dart @@ -168,7 +168,14 @@ class ApiService { Future _connectToUrl(String url) async { _isSessionOnline = false; _onlineCompleter = Completer(); - _chatsFetchedInThisSession = false; + final bool hadChatsFetched = _chatsFetchedInThisSession; + final bool hasValidToken = authToken != null; + + if (!hasValidToken) { + _chatsFetchedInThisSession = false; + } else { + _chatsFetchedInThisSession = hadChatsFetched; + } _connectionStatusController.add('connecting'); @@ -192,7 +199,7 @@ class ApiService { if (proxySettings.isEnabled && proxySettings.host.isNotEmpty) { print( - 'Используем HTTP/HTTPS прокси ${proxySettings.host}:${proxySettings.port}', + 'Используем ${proxySettings.protocol.name.toUpperCase()} прокси ${proxySettings.host}:${proxySettings.port}', ); final customHttpClient = await ProxyService.instance .getHttpClientWithProxy(); @@ -1250,7 +1257,6 @@ class ApiService { void clearChatsCache() { _lastChatsPayload = null; _lastChatsAt = null; - _chatsFetchedInThisSession = false; print("Кэш чатов очищен."); } @@ -1903,9 +1909,9 @@ class ApiService { authToken = token; final prefs = await SharedPreferences.getInstance(); await prefs.setString('authToken', token); - if (_channel != null) { - disconnect(); - } + + disconnect(); + await connect(); await getChatsAndContacts(force: true); if (userId != null) { diff --git a/lib/chats_screen.dart b/lib/chats_screen.dart index 16ff522..30c71fe 100644 --- a/lib/chats_screen.dart +++ b/lib/chats_screen.dart @@ -351,7 +351,7 @@ class _ChatsScreenState extends State final oldChat = _allChats[chatIndex]; if (deletedMessageIds.contains(oldChat.lastMessage.id)) { - ApiService.instance.getChatsAndContacts(force: true).then((data) { + ApiService.instance.getChatsOnly(force: true).then((data) { if (mounted) { final chats = data['chats'] as List; final filtered = chats @@ -3547,7 +3547,6 @@ class _SferumWebViewPanelState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - ), ), ), diff --git a/lib/password_management_screen.dart b/lib/password_management_screen.dart index 632d4b2..39240f6 100644 --- a/lib/password_management_screen.dart +++ b/lib/password_management_screen.dart @@ -39,7 +39,6 @@ class _PasswordManagementScreenState extends State { _apiSubscription = ApiService.instance.messages.listen((message) { if (!mounted) return; - if (message['type'] == 'password_set_success') { setState(() { _isLoading = false; @@ -60,7 +59,6 @@ class _PasswordManagementScreenState extends State { ); } - if (message['cmd'] == 3 && message['opcode'] == 116) { setState(() { _isLoading = false; @@ -147,7 +145,6 @@ class _PasswordManagementScreenState extends State { return; } - if (!password.contains(RegExp(r'[A-Z]')) || !password.contains(RegExp(r'[a-z]'))) { ScaffoldMessenger.of(context).showSnackBar( @@ -166,7 +163,6 @@ class _PasswordManagementScreenState extends State { return; } - if (!password.contains(RegExp(r'[0-9]'))) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -182,7 +178,6 @@ class _PasswordManagementScreenState extends State { return; } - if (!password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -252,7 +247,6 @@ class _PasswordManagementScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -288,7 +282,6 @@ class _PasswordManagementScreenState extends State { const SizedBox(height: 24), - Text( 'Установить пароль', style: Theme.of( @@ -338,7 +331,6 @@ class _PasswordManagementScreenState extends State { ), const SizedBox(height: 16), - Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( diff --git a/lib/phone_entry_screen.dart b/lib/phone_entry_screen.dart index b9a737c..b292357 100644 --- a/lib/phone_entry_screen.dart +++ b/lib/phone_entry_screen.dart @@ -5,6 +5,8 @@ import 'package:flutter/scheduler.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:gwid/api_service.dart'; import 'package:gwid/otp_screen.dart'; +import 'package:gwid/proxy_service.dart'; +import 'package:gwid/screens/settings/proxy_settings_screen.dart'; import 'package:gwid/screens/settings/session_spoofing_screen.dart'; import 'package:gwid/token_auth_screen.dart'; import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS @@ -61,6 +63,7 @@ class _PhoneEntryScreenState extends State bool _isButtonEnabled = false; bool _isLoading = false; bool _hasCustomAnonymity = false; + bool _hasProxyConfigured = false; StreamSubscription? _apiSubscription; bool _showContent = false; bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения @@ -103,6 +106,7 @@ class _PhoneEntryScreenState extends State _initializeMaskFormatter(); _checkAnonymitySettings(); + _checkProxySettings(); _phoneController.addListener(_onPhoneChanged); Future.delayed(const Duration(milliseconds: 300), () { @@ -206,6 +210,19 @@ class _PhoneEntryScreenState extends State if (mounted) setState(() => _hasCustomAnonymity = anonymityEnabled); } + Future _checkProxySettings() async { + final settings = await ProxyService.instance.loadProxySettings(); + if (mounted) { + setState(() { + _hasProxyConfigured = settings.isEnabled && settings.host.isNotEmpty; + }); + } + } + + void refreshProxySettings() { + _checkProxySettings(); + } + void _requestOtp() async { if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return; setState(() => _isLoading = true); @@ -413,6 +430,8 @@ class _PhoneEntryScreenState extends State ), const SizedBox(height: 32), _AnonymityCard(isConfigured: _hasCustomAnonymity), + const SizedBox(height: 16), + _ProxyCard(isConfigured: _hasProxyConfigured), const SizedBox(height: 24), Text.rich( textAlign: TextAlign.center, @@ -664,3 +683,89 @@ class _AnonymityCard extends StatelessWidget { }; } } + +class _ProxyCard extends StatelessWidget { + final bool isConfigured; + const _ProxyCard({required this.isConfigured}); + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + final Color cardColor = isConfigured + ? colors.secondaryContainer + : colors.surfaceContainerHighest.withOpacity(0.5); + final Color onCardColor = isConfigured + ? colors.onSecondaryContainer + : colors.onSurfaceVariant; + final IconData icon = isConfigured ? Icons.vpn_key : Icons.vpn_key_outlined; + + return Card( + elevation: 0, + color: cardColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: colors.outline.withOpacity(0.5)), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + Icon(icon, color: onCardColor, size: 20), + const SizedBox(width: 12), + Expanded( + child: Text( + isConfigured + ? 'Прокси-сервер настроен и активен' + : 'Настройте прокси-сервер для подключения', + style: GoogleFonts.manrope( + textStyle: textTheme.bodyMedium, + color: onCardColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: isConfigured + ? FilledButton.tonalIcon( + onPressed: _navigateToProxyScreen(context), + icon: const Icon(Icons.settings, size: 18), + label: Text( + 'Изменить настройки', + style: GoogleFonts.manrope(fontWeight: FontWeight.bold), + ), + ) + : FilledButton.icon( + onPressed: _navigateToProxyScreen(context), + icon: const Icon(Icons.vpn_key, size: 18), + label: Text( + 'Настроить прокси', + style: GoogleFonts.manrope(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ); + } + + VoidCallback _navigateToProxyScreen(BuildContext context) { + return () async { + await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const ProxySettingsScreen()), + ); + if (context.mounted) { + final state = context.findAncestorStateOfType<_PhoneEntryScreenState>(); + state?.refreshProxySettings(); + } + }; + } +} diff --git a/lib/proxy_service.dart b/lib/proxy_service.dart index f92e114..940baa8 100644 --- a/lib/proxy_service.dart +++ b/lib/proxy_service.dart @@ -1,5 +1,3 @@ - - import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -31,9 +29,14 @@ class ProxyService { return ProxySettings(); } - Future checkProxy(ProxySettings settings) async { print("Проверка прокси: ${settings.host}:${settings.port}"); + + if (settings.protocol == ProxyProtocol.socks5) { + await _checkSocks5Proxy(settings); + return; + } + HttpClient client = _createClientWithOptions(settings); client.connectionTimeout = const Duration(seconds: 10); @@ -47,7 +50,6 @@ class ProxyService { print("Ответ от прокси получен, статус: ${response.statusCode}"); if (response.statusCode >= 400) { - throw Exception('Прокси вернул ошибку: ${response.statusCode}'); } } on HandshakeException catch (e) { @@ -71,39 +73,78 @@ class ProxyService { } } + Future _checkSocks5Proxy(ProxySettings settings) async { + Socket? proxySocket; + try { + print("Проверка SOCKS5 прокси: ${settings.host}:${settings.port}"); + + proxySocket = await Socket.connect( + settings.host, + settings.port, + timeout: const Duration(seconds: 10), + ); + + print("SOCKS5 прокси доступен: ${settings.host}:${settings.port}"); + print( + "Внимание: Полная проверка SOCKS5 требует дополнительной реализации", + ); + + // Закрываем соединение + await proxySocket.close(); + print("SOCKS5 прокси работает корректно"); + } on SocketException catch (e) { + print("Ошибка сокета при проверке SOCKS5 прокси: $e"); + throw Exception('Неверный хост или порт'); + } on TimeoutException catch (_) { + print("Таймаут при проверке SOCKS5 прокси"); + throw Exception('Сервер не отвечает (таймаут)'); + } catch (e) { + print("Ошибка при проверке SOCKS5 прокси: $e"); + throw Exception('Ошибка подключения: ${e.toString()}'); + } finally { + await proxySocket?.close(); + } + } Future getHttpClientWithProxy() async { final settings = await loadProxySettings(); return _createClientWithOptions(settings); } - HttpClient _createClientWithOptions(ProxySettings settings) { final client = HttpClient(); if (settings.isEnabled && settings.host.isNotEmpty) { - print("Используется прокси: ${settings.toFindProxyString()}"); - - client.findProxy = (uri) { - return settings.toFindProxyString(); - }; - - if (settings.username != null && settings.username!.isNotEmpty) { - print( - "Настраивается аутентификация на прокси для пользователя: ${settings.username}", - ); - client.authenticateProxy = (host, port, scheme, realm) async { - client.addProxyCredentials( - host, - port, - realm ?? '', - HttpClientBasicCredentials( - settings.username!, - settings.password ?? '', - ), - ); - return true; + if (settings.protocol == ProxyProtocol.socks5) { + print("Используется SOCKS5 прокси: ${settings.host}:${settings.port}"); + print("Внимание: SOCKS5 для HTTP клиента может работать ограниченно"); + client.findProxy = (uri) { + return settings.toFindProxyString(); }; + } else { + print("Используется прокси: ${settings.toFindProxyString()}"); + + client.findProxy = (uri) { + return settings.toFindProxyString(); + }; + + if (settings.username != null && settings.username!.isNotEmpty) { + print( + "Настраивается аутентификация на прокси для пользователя: ${settings.username}", + ); + client.authenticateProxy = (host, port, scheme, realm) async { + client.addProxyCredentials( + host, + port, + realm ?? '', + HttpClientBasicCredentials( + settings.username!, + settings.password ?? '', + ), + ); + return true; + }; + } } client.badCertificateCallback = diff --git a/lib/screens/settings/proxy_settings_screen.dart b/lib/screens/settings/proxy_settings_screen.dart index e6b2b42..12ec0fc 100644 --- a/lib/screens/settings/proxy_settings_screen.dart +++ b/lib/screens/settings/proxy_settings_screen.dart @@ -1,5 +1,3 @@ - - import 'package:flutter/material.dart'; import 'package:gwid/proxy_service.dart'; import 'package:gwid/proxy_settings.dart'; @@ -40,7 +38,6 @@ class _ProxySettingsScreenState extends State { }); } - Future _testProxyConnection() async { if (_formKey.currentState?.validate() != true) { return; @@ -49,7 +46,6 @@ class _ProxySettingsScreenState extends State { _isTesting = true; }); - final settingsToTest = ProxySettings( isEnabled: true, // Для теста прокси всегда должен быть включен protocol: _settings.protocol, @@ -164,12 +160,7 @@ class _ProxySettingsScreenState extends State { border: OutlineInputBorder(), ), items: ProxyProtocol.values - - .where( - (p) => - p != ProxyProtocol.socks4 && - p != ProxyProtocol.socks5, - ) + .where((p) => p != ProxyProtocol.socks4) .map( (protocol) => DropdownMenuItem( value: protocol, diff --git a/lib/widgets/message_preview_dialog.dart b/lib/widgets/message_preview_dialog.dart index aec191f..511a9d6 100644 --- a/lib/widgets/message_preview_dialog.dart +++ b/lib/widgets/message_preview_dialog.dart @@ -8,6 +8,202 @@ import 'package:gwid/api_service.dart'; import 'package:gwid/widgets/chat_message_bubble.dart'; import 'package:gwid/chat_screen.dart'; +class ControlMessageChip extends StatelessWidget { + final Message message; + final Map contacts; + final int myId; + + const ControlMessageChip({ + super.key, + required this.message, + required this.contacts, + required this.myId, + }); + + String _formatControlMessage() { + final controlAttach = message.attaches.firstWhere( + (a) => a['_type'] == 'CONTROL', + ); + + final eventType = controlAttach['event']; + final senderName = contacts[message.senderId]?.name ?? 'Неизвестный'; + final isMe = message.senderId == myId; + final senderDisplayName = isMe ? 'Вы' : senderName; + + String _formatUserList(List userIds) { + if (userIds.isEmpty) { + return ''; + } + final userNames = userIds + .map((id) { + if (id == myId) { + return 'Вы'; + } + return contacts[id]?.name ?? 'участник с ID $id'; + }) + .where((name) => name.isNotEmpty) + .join(', '); + return userNames; + } + + switch (eventType) { + case 'new': + final title = controlAttach['title'] ?? 'Новая группа'; + return '$senderDisplayName создал(а) группу "$title"'; + + case 'add': + final userIds = List.from( + (controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [], + ); + if (userIds.isEmpty) { + return 'К чату присоединились новые участники'; + } + final userNames = _formatUserList(userIds); + if (userNames.isEmpty) { + return 'К чату присоединились новые участники'; + } + return '$senderDisplayName добавил(а) в чат: $userNames'; + + case 'remove': + case 'kick': + final userIds = List.from( + (controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [], + ); + if (userIds.isEmpty) { + return '$senderDisplayName удалил(а) участников из чата'; + } + final userNames = _formatUserList(userIds); + if (userNames.isEmpty) { + return '$senderDisplayName удалил(а) участников из чата'; + } + + if (userIds.contains(myId)) { + return 'Вы были удалены из чата'; + } + return '$senderDisplayName удалил(а) из чата: $userNames'; + + case 'leave': + if (isMe) { + return 'Вы покинули группу'; + } + return '$senderName покинул(а) группу'; + + case 'title': + final newTitle = controlAttach['title'] ?? ''; + if (newTitle.isEmpty) { + return '$senderDisplayName изменил(а) название группы'; + } + return '$senderDisplayName изменил(а) название группы на "$newTitle"'; + + case 'avatar': + case 'photo': + return '$senderDisplayName изменил(а) фото группы'; + + case 'description': + return '$senderDisplayName изменил(а) описание группы'; + + case 'admin': + case 'promote': + final userIds = List.from( + (controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [], + ); + if (userIds.isEmpty) { + return '$senderDisplayName назначил(а) администраторов'; + } + final userNames = _formatUserList(userIds); + if (userNames.isEmpty) { + return '$senderDisplayName назначил(а) администраторов'; + } + + if (userIds.contains(myId) && userIds.length == 1) { + return 'Вас назначили администратором'; + } + return '$senderDisplayName назначил(а) администраторов: $userNames'; + + case 'demote': + final userIds = List.from( + (controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [], + ); + if (userIds.isEmpty) { + return '$senderDisplayName снял(а) администраторов'; + } + final userNames = _formatUserList(userIds); + if (userNames.isEmpty) { + return '$senderDisplayName снял(а) администраторов'; + } + + if (userIds.contains(myId) && userIds.length == 1) { + return 'С вас сняли права администратора'; + } + return '$senderDisplayName снял(а) права администратора с: $userNames'; + + case 'ban': + final userIds = List.from( + (controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [], + ); + if (userIds.isEmpty) { + return '$senderDisplayName заблокировал(а) участников'; + } + final userNames = _formatUserList(userIds); + if (userNames.isEmpty) { + return '$senderDisplayName заблокировал(а) участников'; + } + + if (userIds.contains(myId)) { + return 'Вы были заблокированы в чате'; + } + return '$senderDisplayName заблокировал(а): $userNames'; + + case 'unban': + final userIds = List.from( + (controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [], + ); + if (userIds.isEmpty) { + return '$senderDisplayName разблокировал(а) участников'; + } + final userNames = _formatUserList(userIds); + if (userNames.isEmpty) { + return '$senderDisplayName разблокировал(а) участников'; + } + return '$senderDisplayName разблокировал(а): $userNames'; + + case 'join': + if (isMe) { + return 'Вы присоединились к группе'; + } + return '$senderName присоединился(ась) к группе'; + + default: + final eventTypeStr = eventType?.toString() ?? 'неизвестное'; + return 'Событие: $eventTypeStr'; + } + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: colors.primaryContainer.withOpacity(0.5), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + _formatControlMessage(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: colors.onPrimaryContainer, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} + class MessagePreviewDialog { static String _formatTimestamp(int timestamp) { final dt = DateTime.fromMillisecondsSinceEpoch(timestamp); @@ -323,6 +519,16 @@ class MessagePreviewDialog { if (item is MessageItem) { final message = item.message; + final isControlMessage = message.attaches.any( + (a) => a['_type'] == 'CONTROL', + ); + if (isControlMessage) { + return ControlMessageChip( + message: message, + contacts: contacts, + myId: myId, + ); + } final isMe = message.senderId == myId; final senderContact = contacts[message.senderId];