исправлено отображение системных действий в предпросмотре, добавлено прокси на экран входа, вроде как добавлена поддержка 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 {
_isSessionOnline = false;
_onlineCompleter = Completer<void>();
_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) {

View File

@@ -351,7 +351,7 @@ class _ChatsScreenState extends State<ChatsScreen>
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<dynamic>;
final filtered = chats
@@ -3547,7 +3547,6 @@ class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),

View File

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

View File

@@ -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<PhoneEntryScreen>
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<PhoneEntryScreen>
_initializeMaskFormatter();
_checkAnonymitySettings();
_checkProxySettings();
_phoneController.addListener(_onPhoneChanged);
Future.delayed(const Duration(milliseconds: 300), () {
@@ -206,6 +210,19 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
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 {
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
setState(() => _isLoading = true);
@@ -413,6 +430,8 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
),
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();
}
};
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -31,9 +29,14 @@ class ProxyService {
return ProxySettings();
}
Future<void> 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<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 {
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 =

View File

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