исправлено отображение системных действий в предпросмотре, добавлено прокси на экран входа, вроде как добавлена поддержка socks5 прокси
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
Reference in New Issue
Block a user