исправлено отображение системных действий в предпросмотре, добавлено прокси на экран входа, вроде как добавлена поддержка socks5 прокси

This commit is contained in:
needle10
2025-11-16 22:47:49 +03:00
parent d479b77ad9
commit fb96dd0996
7 changed files with 392 additions and 52 deletions

View File

@@ -168,7 +168,14 @@ class ApiService {
Future<void> _connectToUrl(String url) async { Future<void> _connectToUrl(String url) async {
_isSessionOnline = false; _isSessionOnline = false;
_onlineCompleter = Completer<void>(); _onlineCompleter = Completer<void>();
_chatsFetchedInThisSession = false; final bool hadChatsFetched = _chatsFetchedInThisSession;
final bool hasValidToken = authToken != null;
if (!hasValidToken) {
_chatsFetchedInThisSession = false;
} else {
_chatsFetchedInThisSession = hadChatsFetched;
}
_connectionStatusController.add('connecting'); _connectionStatusController.add('connecting');
@@ -192,7 +199,7 @@ class ApiService {
if (proxySettings.isEnabled && proxySettings.host.isNotEmpty) { if (proxySettings.isEnabled && proxySettings.host.isNotEmpty) {
print( print(
'Используем HTTP/HTTPS прокси ${proxySettings.host}:${proxySettings.port}', 'Используем ${proxySettings.protocol.name.toUpperCase()} прокси ${proxySettings.host}:${proxySettings.port}',
); );
final customHttpClient = await ProxyService.instance final customHttpClient = await ProxyService.instance
.getHttpClientWithProxy(); .getHttpClientWithProxy();
@@ -1250,7 +1257,6 @@ class ApiService {
void clearChatsCache() { void clearChatsCache() {
_lastChatsPayload = null; _lastChatsPayload = null;
_lastChatsAt = null; _lastChatsAt = null;
_chatsFetchedInThisSession = false;
print("Кэш чатов очищен."); print("Кэш чатов очищен.");
} }
@@ -1903,9 +1909,9 @@ class ApiService {
authToken = token; authToken = token;
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString('authToken', token); await prefs.setString('authToken', token);
if (_channel != null) {
disconnect(); disconnect();
}
await connect(); await connect();
await getChatsAndContacts(force: true); await getChatsAndContacts(force: true);
if (userId != null) { if (userId != null) {

View File

@@ -351,7 +351,7 @@ class _ChatsScreenState extends State<ChatsScreen>
final oldChat = _allChats[chatIndex]; final oldChat = _allChats[chatIndex];
if (deletedMessageIds.contains(oldChat.lastMessage.id)) { if (deletedMessageIds.contains(oldChat.lastMessage.id)) {
ApiService.instance.getChatsAndContacts(force: true).then((data) { ApiService.instance.getChatsOnly(force: true).then((data) {
if (mounted) { if (mounted) {
final chats = data['chats'] as List<dynamic>; final chats = data['chats'] as List<dynamic>;
final filtered = chats final filtered = chats
@@ -3547,7 +3547,6 @@ class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
), ),

View File

@@ -39,7 +39,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
_apiSubscription = ApiService.instance.messages.listen((message) { _apiSubscription = ApiService.instance.messages.listen((message) {
if (!mounted) return; if (!mounted) return;
if (message['type'] == 'password_set_success') { if (message['type'] == 'password_set_success') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -60,7 +59,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
); );
} }
if (message['cmd'] == 3 && message['opcode'] == 116) { if (message['cmd'] == 3 && message['opcode'] == 116) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -147,7 +145,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
return; return;
} }
if (!password.contains(RegExp(r'[A-Z]')) || if (!password.contains(RegExp(r'[A-Z]')) ||
!password.contains(RegExp(r'[a-z]'))) { !password.contains(RegExp(r'[a-z]'))) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -166,7 +163,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
return; return;
} }
if (!password.contains(RegExp(r'[0-9]'))) { if (!password.contains(RegExp(r'[0-9]'))) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -182,7 +178,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
return; return;
} }
if (!password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) { if (!password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -252,7 +247,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -288,7 +282,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Установить пароль', 'Установить пароль',
style: Theme.of( style: Theme.of(
@@ -338,7 +331,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -5,6 +5,8 @@ import 'package:flutter/scheduler.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:gwid/api_service.dart'; import 'package:gwid/api_service.dart';
import 'package:gwid/otp_screen.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/screens/settings/session_spoofing_screen.dart';
import 'package:gwid/token_auth_screen.dart'; import 'package:gwid/token_auth_screen.dart';
import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS
@@ -61,6 +63,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
bool _isButtonEnabled = false; bool _isButtonEnabled = false;
bool _isLoading = false; bool _isLoading = false;
bool _hasCustomAnonymity = false; bool _hasCustomAnonymity = false;
bool _hasProxyConfigured = false;
StreamSubscription? _apiSubscription; StreamSubscription? _apiSubscription;
bool _showContent = false; bool _showContent = false;
bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения
@@ -103,6 +106,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
_initializeMaskFormatter(); _initializeMaskFormatter();
_checkAnonymitySettings(); _checkAnonymitySettings();
_checkProxySettings();
_phoneController.addListener(_onPhoneChanged); _phoneController.addListener(_onPhoneChanged);
Future.delayed(const Duration(milliseconds: 300), () { Future.delayed(const Duration(milliseconds: 300), () {
@@ -206,6 +210,19 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
if (mounted) setState(() => _hasCustomAnonymity = anonymityEnabled); if (mounted) setState(() => _hasCustomAnonymity = anonymityEnabled);
} }
Future<void> _checkProxySettings() async {
final settings = await ProxyService.instance.loadProxySettings();
if (mounted) {
setState(() {
_hasProxyConfigured = settings.isEnabled && settings.host.isNotEmpty;
});
}
}
void refreshProxySettings() {
_checkProxySettings();
}
void _requestOtp() async { void _requestOtp() async {
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return; if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
setState(() => _isLoading = true); setState(() => _isLoading = true);
@@ -413,6 +430,8 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
_AnonymityCard(isConfigured: _hasCustomAnonymity), _AnonymityCard(isConfigured: _hasCustomAnonymity),
const SizedBox(height: 16),
_ProxyCard(isConfigured: _hasProxyConfigured),
const SizedBox(height: 24), const SizedBox(height: 24),
Text.rich( Text.rich(
textAlign: TextAlign.center, 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();
}
};
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
@@ -31,9 +29,14 @@ class ProxyService {
return ProxySettings(); return ProxySettings();
} }
Future<void> checkProxy(ProxySettings settings) async { Future<void> checkProxy(ProxySettings settings) async {
print("Проверка прокси: ${settings.host}:${settings.port}"); print("Проверка прокси: ${settings.host}:${settings.port}");
if (settings.protocol == ProxyProtocol.socks5) {
await _checkSocks5Proxy(settings);
return;
}
HttpClient client = _createClientWithOptions(settings); HttpClient client = _createClientWithOptions(settings);
client.connectionTimeout = const Duration(seconds: 10); client.connectionTimeout = const Duration(seconds: 10);
@@ -47,7 +50,6 @@ class ProxyService {
print("Ответ от прокси получен, статус: ${response.statusCode}"); print("Ответ от прокси получен, статус: ${response.statusCode}");
if (response.statusCode >= 400) { if (response.statusCode >= 400) {
throw Exception('Прокси вернул ошибку: ${response.statusCode}'); throw Exception('Прокси вернул ошибку: ${response.statusCode}');
} }
} on HandshakeException catch (e) { } on HandshakeException catch (e) {
@@ -71,39 +73,78 @@ class ProxyService {
} }
} }
Future<void> _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<HttpClient> getHttpClientWithProxy() async { Future<HttpClient> getHttpClientWithProxy() async {
final settings = await loadProxySettings(); final settings = await loadProxySettings();
return _createClientWithOptions(settings); return _createClientWithOptions(settings);
} }
HttpClient _createClientWithOptions(ProxySettings settings) { HttpClient _createClientWithOptions(ProxySettings settings) {
final client = HttpClient(); final client = HttpClient();
if (settings.isEnabled && settings.host.isNotEmpty) { if (settings.isEnabled && settings.host.isNotEmpty) {
print("Используется прокси: ${settings.toFindProxyString()}"); if (settings.protocol == ProxyProtocol.socks5) {
print("Используется SOCKS5 прокси: ${settings.host}:${settings.port}");
client.findProxy = (uri) { print("Внимание: SOCKS5 для HTTP клиента может работать ограниченно");
return 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;
}; };
} 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 = client.badCertificateCallback =

View File

@@ -1,5 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gwid/proxy_service.dart'; import 'package:gwid/proxy_service.dart';
import 'package:gwid/proxy_settings.dart'; import 'package:gwid/proxy_settings.dart';
@@ -40,7 +38,6 @@ class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
}); });
} }
Future<void> _testProxyConnection() async { Future<void> _testProxyConnection() async {
if (_formKey.currentState?.validate() != true) { if (_formKey.currentState?.validate() != true) {
return; return;
@@ -49,7 +46,6 @@ class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
_isTesting = true; _isTesting = true;
}); });
final settingsToTest = ProxySettings( final settingsToTest = ProxySettings(
isEnabled: true, // Для теста прокси всегда должен быть включен isEnabled: true, // Для теста прокси всегда должен быть включен
protocol: _settings.protocol, protocol: _settings.protocol,
@@ -164,12 +160,7 @@ class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
items: ProxyProtocol.values items: ProxyProtocol.values
.where((p) => p != ProxyProtocol.socks4)
.where(
(p) =>
p != ProxyProtocol.socks4 &&
p != ProxyProtocol.socks5,
)
.map( .map(
(protocol) => DropdownMenuItem( (protocol) => DropdownMenuItem(
value: protocol, value: protocol,

View File

@@ -8,6 +8,202 @@ import 'package:gwid/api_service.dart';
import 'package:gwid/widgets/chat_message_bubble.dart'; import 'package:gwid/widgets/chat_message_bubble.dart';
import 'package:gwid/chat_screen.dart'; import 'package:gwid/chat_screen.dart';
class ControlMessageChip extends StatelessWidget {
final Message message;
final Map<int, Contact> 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<int> 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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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 { class MessagePreviewDialog {
static String _formatTimestamp(int timestamp) { static String _formatTimestamp(int timestamp) {
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp); final dt = DateTime.fromMillisecondsSinceEpoch(timestamp);
@@ -323,6 +519,16 @@ class MessagePreviewDialog {
if (item is MessageItem) { if (item is MessageItem) {
final message = item.message; 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 isMe = message.senderId == myId;
final senderContact = final senderContact =
contacts[message.senderId]; contacts[message.senderId];