v0.4.0: Merge pull request #1 from KometTeam/reorg
v0.4.0: Codebase reorganization, tons of small fixes, registration and workable context menus on Desktop
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -43,3 +43,7 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
pubspec.lock
|
||||
.vscode/
|
||||
macos/Podfile.lock
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"cmake.sourceDirectory": "C:/Users/Kiril/OneDrive/Desktop/KOMET/KOMETABLYATSKAYA/app/linux"
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
43
ios/Podfile
Normal file
43
ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
753
lib/api/api_registration_service.dart
Normal file
753
lib/api/api_registration_service.dart
Normal file
@@ -0,0 +1,753 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:es_compression/lz4.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:msgpack_dart/msgpack_dart.dart' as msgpack;
|
||||
|
||||
// FFI типы для LZ4 block decompress
|
||||
typedef Lz4DecompressFunction =
|
||||
Int32 Function(
|
||||
Pointer<Uint8> src,
|
||||
Pointer<Uint8> dst,
|
||||
Int32 compressedSize,
|
||||
Int32 dstCapacity,
|
||||
);
|
||||
typedef Lz4Decompress =
|
||||
int Function(
|
||||
Pointer<Uint8> src,
|
||||
Pointer<Uint8> dst,
|
||||
int compressedSize,
|
||||
int dstCapacity,
|
||||
);
|
||||
|
||||
class RegistrationService {
|
||||
Socket? _socket;
|
||||
int _seq = 0;
|
||||
final Map<int, Completer<dynamic>> _pending = {};
|
||||
bool _isConnected = false;
|
||||
Timer? _pingTimer;
|
||||
StreamSubscription? _socketSubscription;
|
||||
Lz4Codec? _lz4Codec;
|
||||
DynamicLibrary? _lz4Lib;
|
||||
Lz4Decompress? _lz4BlockDecompress;
|
||||
|
||||
void _initLz4BlockDecompress() {
|
||||
if (_lz4BlockDecompress != null) return;
|
||||
|
||||
try {
|
||||
if (Platform.isWindows) {
|
||||
// Пробуем загрузить eslz4-win64.dll
|
||||
final dllPath = 'eslz4-win64.dll';
|
||||
print('📦 Загрузка LZ4 DLL для block decompress: $dllPath');
|
||||
_lz4Lib = DynamicLibrary.open(dllPath);
|
||||
|
||||
// Ищем функцию LZ4_decompress_safe (block format)
|
||||
try {
|
||||
_lz4BlockDecompress = _lz4Lib!
|
||||
.lookup<NativeFunction<Lz4DecompressFunction>>(
|
||||
'LZ4_decompress_safe',
|
||||
)
|
||||
.asFunction();
|
||||
print('✅ LZ4 block decompress функция загружена');
|
||||
} catch (e) {
|
||||
print(
|
||||
'⚠️ Функция LZ4_decompress_safe не найдена, пробуем альтернативные имена...',
|
||||
);
|
||||
// Пробуем другие возможные имена
|
||||
try {
|
||||
_lz4BlockDecompress = _lz4Lib!
|
||||
.lookup<NativeFunction<Lz4DecompressFunction>>(
|
||||
'LZ4_decompress_fast',
|
||||
)
|
||||
.asFunction();
|
||||
print('✅ LZ4 block decompress функция загружена (fast)');
|
||||
} catch (e2) {
|
||||
print('❌ Не удалось найти LZ4 block decompress функцию: $e2');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('⚠️ Не удалось загрузить LZ4 DLL для block decompress: $e');
|
||||
print('📦 Будем использовать только frame format (es_compression)');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
if (_isConnected) return;
|
||||
|
||||
// Инициализируем LZ4 block decompress
|
||||
_initLz4BlockDecompress();
|
||||
|
||||
try {
|
||||
print('🌐 Подключаемся к api.oneme.ru:443...');
|
||||
|
||||
// Создаем SSL контекст
|
||||
final securityContext = SecurityContext.defaultContext;
|
||||
|
||||
print('🔒 Создаем TCP соединение...');
|
||||
final rawSocket = await Socket.connect('api.oneme.ru', 443);
|
||||
print('✅ TCP соединение установлено');
|
||||
|
||||
print('🔒 Устанавливаем SSL соединение...');
|
||||
_socket = await SecureSocket.secure(
|
||||
rawSocket,
|
||||
context: securityContext,
|
||||
host: 'api.oneme.ru',
|
||||
onBadCertificate: (certificate) {
|
||||
print('⚠️ Сертификат не прошел проверку, принимаем...');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
print('✅ SSL соединение установлено');
|
||||
|
||||
// Запускаем ping loop
|
||||
_startPingLoop();
|
||||
|
||||
// Слушаем ответы
|
||||
_socketSubscription = _socket!.listen(
|
||||
_handleData,
|
||||
onError: (error) {
|
||||
print('❌ Ошибка сокета: $error');
|
||||
_isConnected = false;
|
||||
},
|
||||
onDone: () {
|
||||
print('🔌 Соединение закрыто');
|
||||
_isConnected = false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
print('❌ Ошибка подключения: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _startPingLoop() {
|
||||
_pingTimer?.cancel();
|
||||
_pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (!_isConnected) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _sendMessage(1, {});
|
||||
print('🏓 Ping отправлен');
|
||||
} catch (e) {
|
||||
print('❌ Ping failed: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleData(Uint8List data) {
|
||||
// Обрабатываем данные по частям - сначала заголовок, потом payload
|
||||
_processIncomingData(data);
|
||||
}
|
||||
|
||||
Uint8List? _buffer = Uint8List(0);
|
||||
|
||||
void _processIncomingData(Uint8List newData) {
|
||||
// Добавляем новые данные в буфер
|
||||
_buffer = Uint8List.fromList([..._buffer!, ...newData]);
|
||||
|
||||
while (_buffer!.length >= 10) {
|
||||
// Читаем заголовок
|
||||
final header = _buffer!.sublist(0, 10);
|
||||
final payloadLen =
|
||||
ByteData.view(header.buffer, 6, 4).getUint32(0, Endian.big) &
|
||||
0xFFFFFF;
|
||||
|
||||
if (_buffer!.length < 10 + payloadLen) {
|
||||
// Недостаточно данных, ждем еще
|
||||
break;
|
||||
}
|
||||
|
||||
// Полный пакет готов
|
||||
final fullPacket = _buffer!.sublist(0, 10 + payloadLen);
|
||||
_buffer = _buffer!.sublist(10 + payloadLen);
|
||||
|
||||
_processPacket(fullPacket);
|
||||
}
|
||||
}
|
||||
|
||||
void _processPacket(Uint8List packet) {
|
||||
try {
|
||||
// Разбираем заголовок
|
||||
final ver = packet[0];
|
||||
final cmd = ByteData.view(packet.buffer).getUint16(1, Endian.big);
|
||||
final seq = packet[3];
|
||||
final opcode = ByteData.view(packet.buffer).getUint16(4, Endian.big);
|
||||
final packedLen = ByteData.view(
|
||||
packet.buffer,
|
||||
6,
|
||||
4,
|
||||
).getUint32(0, Endian.big);
|
||||
|
||||
// Проверяем флаг сжатия (как в packet_framer.dart)
|
||||
final compFlag = packedLen >> 24;
|
||||
final payloadLen = packedLen & 0x00FFFFFF;
|
||||
|
||||
print('═══════════════════════════════════════════════════════════');
|
||||
print('📥 ПОЛУЧЕН ПАКЕТ ОТ СЕРВЕРА');
|
||||
print('═══════════════════════════════════════════════════════════');
|
||||
print(
|
||||
'📋 Заголовок: ver=$ver, cmd=$cmd, seq=$seq, opcode=$opcode, packedLen=$packedLen, compFlag=$compFlag, payloadLen=$payloadLen',
|
||||
);
|
||||
print('📦 Полный пакет (hex, ${packet.length} байт):');
|
||||
print(_bytesToHex(packet));
|
||||
print('');
|
||||
|
||||
final payloadBytes = packet.sublist(10, 10 + payloadLen);
|
||||
print('📦 Сырые payload байты (hex, ${payloadBytes.length} байт):');
|
||||
print(_bytesToHex(payloadBytes));
|
||||
print('');
|
||||
|
||||
final payload = _unpackPacketPayload(payloadBytes, compFlag != 0);
|
||||
|
||||
print('📦 Разобранный payload (после LZ4 и msgpack):');
|
||||
print(_formatPayload(payload));
|
||||
print('═══════════════════════════════════════════════════════════');
|
||||
print('');
|
||||
|
||||
// Находим completer по seq
|
||||
final completer = _pending[seq];
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
completer.complete(payload);
|
||||
print('✅ Completer завершен для seq=$seq');
|
||||
} else {
|
||||
print('⚠️ Completer не найден для seq=$seq');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Ошибка разбора пакета: $e');
|
||||
print('Stack trace: ${StackTrace.current}');
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _packPacket(
|
||||
int ver,
|
||||
int cmd,
|
||||
int seq,
|
||||
int opcode,
|
||||
Map<String, dynamic> payload,
|
||||
) {
|
||||
final verB = Uint8List(1)..[0] = ver;
|
||||
final cmdB = Uint8List(2)
|
||||
..buffer.asByteData().setUint16(0, cmd, Endian.big);
|
||||
final seqB = Uint8List(1)..[0] = seq;
|
||||
final opcodeB = Uint8List(2)
|
||||
..buffer.asByteData().setUint16(0, opcode, Endian.big);
|
||||
|
||||
final payloadBytes = msgpack.serialize(payload);
|
||||
final payloadLen = payloadBytes.length & 0xFFFFFF;
|
||||
final payloadLenB = Uint8List(4)
|
||||
..buffer.asByteData().setUint32(0, payloadLen, Endian.big);
|
||||
|
||||
final packet = Uint8List.fromList(
|
||||
verB + cmdB + seqB + opcodeB + payloadLenB + payloadBytes,
|
||||
);
|
||||
|
||||
print('═══════════════════════════════════════════════════════════');
|
||||
print('📤 ОТПРАВЛЯЕМ ПАКЕТ НА СЕРВЕР');
|
||||
print('═══════════════════════════════════════════════════════════');
|
||||
print(
|
||||
'📋 Заголовок: ver=$ver, cmd=$cmd, seq=$seq, opcode=$opcode, payloadLen=$payloadLen',
|
||||
);
|
||||
print('📦 Payload (JSON):');
|
||||
print(_formatPayload(payload));
|
||||
print('📦 Payload (msgpack hex, ${payloadBytes.length} байт):');
|
||||
print(_bytesToHex(payloadBytes));
|
||||
print('📦 Полный пакет (hex, ${packet.length} байт):');
|
||||
print(_bytesToHex(packet));
|
||||
print('═══════════════════════════════════════════════════════════');
|
||||
print('');
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
String _bytesToHex(Uint8List bytes) {
|
||||
final buffer = StringBuffer();
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
if (i > 0 && i % 16 == 0) buffer.writeln();
|
||||
buffer.write(bytes[i].toRadixString(16).padLeft(2, '0').toUpperCase());
|
||||
buffer.write(' ');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String _formatPayload(dynamic payload) {
|
||||
if (payload == null) return 'null';
|
||||
if (payload is Map) {
|
||||
final buffer = StringBuffer();
|
||||
_formatMap(payload, buffer, 0);
|
||||
return buffer.toString();
|
||||
}
|
||||
return payload.toString();
|
||||
}
|
||||
|
||||
void _formatMap(Map map, StringBuffer buffer, int indent) {
|
||||
final indentStr = ' ' * indent;
|
||||
buffer.writeln('{');
|
||||
map.forEach((key, value) {
|
||||
buffer.write('$indentStr "$key": ');
|
||||
if (value is Map) {
|
||||
_formatMap(value, buffer, indent + 1);
|
||||
} else if (value is List) {
|
||||
buffer.writeln('[');
|
||||
for (var item in value) {
|
||||
buffer.write('$indentStr ');
|
||||
if (item is Map) {
|
||||
_formatMap(item, buffer, indent + 2);
|
||||
} else {
|
||||
buffer.writeln('$item,');
|
||||
}
|
||||
}
|
||||
buffer.writeln('$indentStr ],');
|
||||
} else {
|
||||
buffer.writeln('$value,');
|
||||
}
|
||||
});
|
||||
buffer.write('$indentStr}');
|
||||
if (indent > 0) buffer.writeln(',');
|
||||
}
|
||||
|
||||
dynamic _deserializeMsgpack(Uint8List data) {
|
||||
print('📦 Десериализация msgpack...');
|
||||
try {
|
||||
final payload = msgpack.deserialize(data);
|
||||
print('✅ Msgpack десериализация успешна');
|
||||
|
||||
// Проверяем, что получили валидный результат (не просто число)
|
||||
if (payload is int && payload < 0) {
|
||||
print(
|
||||
'⚠️ Получено отрицательное число вместо Map - возможно данные все еще сжаты',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
} catch (e) {
|
||||
print('❌ Ошибка десериализации msgpack: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _unpackPacketPayload(
|
||||
Uint8List payloadBytes, [
|
||||
bool isCompressed = false,
|
||||
]) {
|
||||
if (payloadBytes.isEmpty) {
|
||||
print('📦 Payload пустой');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Сначала пробуем LZ4 декомпрессию как в register.py
|
||||
Uint8List decompressedBytes = payloadBytes;
|
||||
|
||||
// Если данные сжаты (compFlag != 0), пробуем LZ4 block декомпрессию
|
||||
if (isCompressed && payloadBytes.length > 4) {
|
||||
print('📦 Данные помечены как сжатые (compFlag != 0)');
|
||||
|
||||
// Пробуем LZ4 block декомпрессию через FFI (как в register.py)
|
||||
try {
|
||||
if (_lz4BlockDecompress != null) {
|
||||
print('📦 Попытка LZ4 block декомпрессии через FFI...');
|
||||
|
||||
// В register.py используется фиксированный uncompressed_size=99999
|
||||
// И данные используются полностью (без пропуска первых 4 байт)
|
||||
// Но в packet_framer.dart при compFlag пропускаются первые 4 байта
|
||||
// Попробуем оба варианта
|
||||
|
||||
// Вариант 1: как в register.py - используем все данные с фиксированным размером
|
||||
// Увеличиваем размер для больших ответов (как в register.py используется 99999, но может быть недостаточно)
|
||||
int uncompressedSize =
|
||||
500000; // Увеличенный размер для больших ответов
|
||||
Uint8List compressedData = payloadBytes;
|
||||
|
||||
print(
|
||||
'📦 Попытка 1: Используем все данные с uncompressed_size=99999 (как в register.py)',
|
||||
);
|
||||
try {
|
||||
if (uncompressedSize > 0 && uncompressedSize < 10 * 1024 * 1024) {
|
||||
final srcSize = compressedData.length;
|
||||
final srcPtr = malloc.allocate<Uint8>(srcSize);
|
||||
final dstPtr = malloc.allocate<Uint8>(uncompressedSize);
|
||||
|
||||
try {
|
||||
final srcList = srcPtr.asTypedList(srcSize);
|
||||
srcList.setAll(0, compressedData);
|
||||
|
||||
final result = _lz4BlockDecompress!(
|
||||
srcPtr,
|
||||
dstPtr,
|
||||
srcSize,
|
||||
uncompressedSize,
|
||||
);
|
||||
|
||||
if (result > 0) {
|
||||
final actualSize = result;
|
||||
final dstList = dstPtr.asTypedList(actualSize);
|
||||
decompressedBytes = Uint8List.fromList(dstList);
|
||||
print(
|
||||
'✅ LZ4 block декомпрессия успешна: $srcSize → ${decompressedBytes.length} байт',
|
||||
);
|
||||
print(
|
||||
'📦 Декомпрессированные данные (hex, первые 64 байта):',
|
||||
);
|
||||
final preview = decompressedBytes.length > 64
|
||||
? decompressedBytes.sublist(0, 64)
|
||||
: decompressedBytes;
|
||||
print(_bytesToHex(preview));
|
||||
// Успешная декомпрессия - возвращаем результат
|
||||
return _deserializeMsgpack(decompressedBytes);
|
||||
} else {
|
||||
throw Exception('LZ4 декомпрессия вернула ошибку: $result');
|
||||
}
|
||||
} finally {
|
||||
malloc.free(srcPtr);
|
||||
malloc.free(dstPtr);
|
||||
}
|
||||
}
|
||||
} catch (e1) {
|
||||
print('⚠️ Вариант 1 не сработал: $e1');
|
||||
|
||||
// Вариант 2: пропускаем первые 4 байта (как в packet_framer.dart)
|
||||
if (payloadBytes.length > 4) {
|
||||
print('📦 Попытка 2: Пропускаем первые 4 байта...');
|
||||
compressedData = payloadBytes.sublist(4);
|
||||
print('📦 Сжатые данные (hex, первые 32 байта):');
|
||||
final firstBytes = compressedData.length > 32
|
||||
? compressedData.sublist(0, 32)
|
||||
: compressedData;
|
||||
print(_bytesToHex(firstBytes));
|
||||
|
||||
try {
|
||||
final srcSize = compressedData.length;
|
||||
final srcPtr = malloc.allocate<Uint8>(srcSize);
|
||||
final dstPtr = malloc.allocate<Uint8>(uncompressedSize);
|
||||
|
||||
try {
|
||||
final srcList = srcPtr.asTypedList(srcSize);
|
||||
srcList.setAll(0, compressedData);
|
||||
|
||||
final result = _lz4BlockDecompress!(
|
||||
srcPtr,
|
||||
dstPtr,
|
||||
srcSize,
|
||||
uncompressedSize,
|
||||
);
|
||||
|
||||
if (result > 0) {
|
||||
final actualSize = result;
|
||||
final dstList = dstPtr.asTypedList(actualSize);
|
||||
decompressedBytes = Uint8List.fromList(dstList);
|
||||
print(
|
||||
'✅ LZ4 block декомпрессия успешна (вариант 2): $srcSize → ${decompressedBytes.length} байт',
|
||||
);
|
||||
print(
|
||||
'📦 Декомпрессированные данные (hex, первые 64 байта):',
|
||||
);
|
||||
final preview = decompressedBytes.length > 64
|
||||
? decompressedBytes.sublist(0, 64)
|
||||
: decompressedBytes;
|
||||
print(_bytesToHex(preview));
|
||||
// Успешная декомпрессия - возвращаем результат
|
||||
return _deserializeMsgpack(decompressedBytes);
|
||||
} else {
|
||||
throw Exception(
|
||||
'LZ4 декомпрессия вернула ошибку: $result',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
malloc.free(srcPtr);
|
||||
malloc.free(dstPtr);
|
||||
}
|
||||
} catch (e2) {
|
||||
print('⚠️ Вариант 2 не сработал: $e2');
|
||||
throw e2; // Пробрасываем ошибку дальше
|
||||
}
|
||||
} else {
|
||||
throw e1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Пробуем через es_compression (frame format)
|
||||
final compressedData = payloadBytes.sublist(4);
|
||||
if (_lz4Codec == null) {
|
||||
print('📦 Инициализация Lz4Codec (frame format)...');
|
||||
_lz4Codec = Lz4Codec();
|
||||
print('✅ Lz4Codec инициализирован успешно');
|
||||
}
|
||||
|
||||
print('📦 Попытка декомпрессии через es_compression...');
|
||||
final decoded = _lz4Codec!.decode(compressedData);
|
||||
decompressedBytes = decoded is Uint8List
|
||||
? decoded
|
||||
: Uint8List.fromList(decoded);
|
||||
print(
|
||||
'✅ LZ4 декомпрессия успешна: ${compressedData.length} → ${decompressedBytes.length} байт',
|
||||
);
|
||||
}
|
||||
} catch (lz4Error) {
|
||||
print('⚠️ LZ4 декомпрессия не применена: $lz4Error');
|
||||
print('📦 Тип ошибки: ${lz4Error.runtimeType}');
|
||||
print('📦 Используем сырые данные...');
|
||||
decompressedBytes = payloadBytes;
|
||||
}
|
||||
} else {
|
||||
// Данные не сжаты или нет флага - пробуем LZ4 на всякий случай (как в register.py)
|
||||
print(
|
||||
'📦 Данные не помечены как сжатые, но пробуем LZ4 (как в register.py)...',
|
||||
);
|
||||
final firstBytes = payloadBytes.length > 32
|
||||
? payloadBytes.sublist(0, 32)
|
||||
: payloadBytes;
|
||||
print(
|
||||
'📦 Первые ${firstBytes.length} байта payload (hex): ${_bytesToHex(firstBytes)}',
|
||||
);
|
||||
|
||||
try {
|
||||
if (_lz4Codec == null) {
|
||||
print('📦 Инициализация Lz4Codec...');
|
||||
_lz4Codec = Lz4Codec();
|
||||
print('✅ Lz4Codec инициализирован успешно');
|
||||
}
|
||||
|
||||
print('📦 Попытка декомпрессии ${payloadBytes.length} байт...');
|
||||
final decoded = _lz4Codec!.decode(payloadBytes);
|
||||
decompressedBytes = decoded is Uint8List
|
||||
? decoded
|
||||
: Uint8List.fromList(decoded);
|
||||
print(
|
||||
'✅ LZ4 декомпрессия успешна: ${payloadBytes.length} → ${decompressedBytes.length} байт',
|
||||
);
|
||||
} catch (lz4Error) {
|
||||
// Если LZ4 не удалась (данные не сжаты), используем сырые данные
|
||||
print(
|
||||
'⚠️ LZ4 декомпрессия не применена (данные не сжаты): $lz4Error',
|
||||
);
|
||||
decompressedBytes = payloadBytes;
|
||||
}
|
||||
}
|
||||
|
||||
return _deserializeMsgpack(decompressedBytes);
|
||||
} catch (e) {
|
||||
print('❌ Ошибка десериализации payload: $e');
|
||||
print('Stack trace: ${StackTrace.current}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _sendMessage(int opcode, Map<String, dynamic> payload) async {
|
||||
if (!_isConnected || _socket == null) {
|
||||
throw Exception('Не подключено к серверу');
|
||||
}
|
||||
|
||||
_seq = (_seq + 1) % 256;
|
||||
final seq = _seq;
|
||||
final packet = _packPacket(10, 0, seq, opcode, payload);
|
||||
|
||||
print('📤 Отправляем сообщение opcode=$opcode, seq=$seq');
|
||||
|
||||
final completer = Completer<dynamic>();
|
||||
_pending[seq] = completer;
|
||||
|
||||
_socket!.add(packet);
|
||||
await _socket!.flush();
|
||||
|
||||
return completer.future.timeout(const Duration(seconds: 30));
|
||||
}
|
||||
|
||||
Future<String> startRegistration(String phoneNumber) async {
|
||||
await connect();
|
||||
|
||||
// Отправляем handshake
|
||||
final handshakePayload = {
|
||||
"mt_instanceid": "63ae21a8-2417-484d-849b-0ae464a7b352",
|
||||
"userAgent": {
|
||||
"deviceType": "ANDROID",
|
||||
"appVersion": "25.14.2",
|
||||
"osVersion": "Android 14",
|
||||
"timezone": "Europe/Moscow",
|
||||
"screen": "440dpi 440dpi 1080x2072",
|
||||
"pushDeviceType": "GCM",
|
||||
"arch": "x86_64",
|
||||
"locale": "ru",
|
||||
"buildNumber": 6442,
|
||||
"deviceName": "unknown Android SDK built for x86_64",
|
||||
"deviceLocale": "en",
|
||||
},
|
||||
"clientSessionId": 8,
|
||||
"deviceId": "d53058ab998c3bdd",
|
||||
};
|
||||
|
||||
print('🤝 Отправляем handshake (opcode=6)...');
|
||||
print('📦 Handshake payload:');
|
||||
print(_formatPayload(handshakePayload));
|
||||
final handshakeResponse = await _sendMessage(6, handshakePayload);
|
||||
print('📨 Ответ от handshake:');
|
||||
print(_formatPayload(handshakeResponse));
|
||||
|
||||
// Проверяем ошибки
|
||||
if (handshakeResponse is Map) {
|
||||
final err = handshakeResponse['payload']?['error'];
|
||||
if (err != null) {
|
||||
print('❌ Ошибка handshake: $err');
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем START_AUTH
|
||||
final authPayload = {"type": "START_AUTH", "phone": phoneNumber};
|
||||
print('🚀 Отправляем START_AUTH (opcode=17)...');
|
||||
print('📦 START_AUTH payload:');
|
||||
print(_formatPayload(authPayload));
|
||||
final response = await _sendMessage(17, authPayload);
|
||||
|
||||
print('📨 Ответ от START_AUTH:');
|
||||
print(_formatPayload(response));
|
||||
|
||||
// Проверяем ошибки
|
||||
if (response is Map) {
|
||||
// Проверяем ошибку в payload или в корне ответа
|
||||
final payload = response['payload'] ?? response;
|
||||
final err = payload['error'] ?? response['error'];
|
||||
|
||||
if (err != null) {
|
||||
// Обрабатываем конкретную ошибку limit.violate
|
||||
if (err.toString().contains('limit.violate') ||
|
||||
err.toString().contains('error.limit.violate')) {
|
||||
throw Exception(
|
||||
'У вас кончились попытки на код, попробуйте позже...',
|
||||
);
|
||||
}
|
||||
|
||||
// Для других ошибок используем сообщение от сервера или общее
|
||||
final message =
|
||||
payload['localizedMessage'] ??
|
||||
payload['message'] ??
|
||||
payload['description'] ??
|
||||
'Ошибка START_AUTH: $err';
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Извлекаем токен из ответа (как в register.py)
|
||||
if (response is Map) {
|
||||
final payload = response['payload'] ?? response;
|
||||
final token = payload['token'] ?? response['token'];
|
||||
if (token != null) {
|
||||
return token as String;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Не удалось получить токен из ответа сервера');
|
||||
}
|
||||
|
||||
Future<String> verifyCode(String token, String code) async {
|
||||
final verifyPayload = {
|
||||
"verifyCode": code,
|
||||
"token": token,
|
||||
"authTokenType": "CHECK_CODE",
|
||||
};
|
||||
|
||||
print('🔍 Проверяем код (opcode=18)...');
|
||||
print('📦 CHECK_CODE payload:');
|
||||
print(_formatPayload(verifyPayload));
|
||||
final response = await _sendMessage(18, verifyPayload);
|
||||
|
||||
print('📨 Ответ от CHECK_CODE:');
|
||||
print(_formatPayload(response));
|
||||
|
||||
// Проверяем ошибки
|
||||
if (response is Map) {
|
||||
// Проверяем ошибку в payload или в корне ответа
|
||||
final payload = response['payload'] ?? response;
|
||||
final err = payload['error'] ?? response['error'];
|
||||
|
||||
if (err != null) {
|
||||
// Обрабатываем конкретную ошибку неправильного кода
|
||||
if (err.toString().contains('verify.code.wrong') ||
|
||||
err.toString().contains('wrong.code') ||
|
||||
err.toString().contains('code.wrong')) {
|
||||
throw Exception('Неверный код');
|
||||
}
|
||||
|
||||
// Для других ошибок используем сообщение от сервера или общее
|
||||
final message =
|
||||
payload['localizedMessage'] ??
|
||||
payload['message'] ??
|
||||
payload['title'] ??
|
||||
'Ошибка CHECK_CODE: $err';
|
||||
throw Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Извлекаем register токен (как в register.py)
|
||||
if (response is Map) {
|
||||
final tokenSrc = response['payload'] ?? response;
|
||||
final tokenAttrs = tokenSrc['tokenAttrs'];
|
||||
|
||||
// Проверяем, есть ли LOGIN токен - значит аккаунт уже существует
|
||||
if (tokenAttrs is Map && tokenAttrs['LOGIN'] is Map) {
|
||||
throw Exception('ACCOUNT_EXISTS');
|
||||
}
|
||||
|
||||
if (tokenAttrs is Map && tokenAttrs['REGISTER'] is Map) {
|
||||
final registerToken = tokenAttrs['REGISTER']['token'];
|
||||
if (registerToken != null) {
|
||||
return registerToken as String;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Не удалось получить токен регистрации из ответа сервера');
|
||||
}
|
||||
|
||||
Future<void> completeRegistration(String registerToken) async {
|
||||
final registerPayload = {
|
||||
"lastName": "User",
|
||||
"token": registerToken,
|
||||
"firstName": "Komet",
|
||||
"tokenType": "REGISTER",
|
||||
};
|
||||
|
||||
print('🎉 Завершаем регистрацию (opcode=23)...');
|
||||
print('📦 REGISTER payload:');
|
||||
print(_formatPayload(registerPayload));
|
||||
final response = await _sendMessage(23, registerPayload);
|
||||
|
||||
print('📨 Ответ от REGISTER:');
|
||||
print(_formatPayload(response));
|
||||
|
||||
// Проверяем ошибки
|
||||
if (response is Map) {
|
||||
final err = response['payload']?['error'];
|
||||
if (err != null) {
|
||||
throw Exception('Ошибка REGISTER: $err');
|
||||
}
|
||||
|
||||
// Извлекаем финальный токен
|
||||
final payload = response['payload'] ?? response;
|
||||
final finalToken = payload['token'] ?? response['token'];
|
||||
if (finalToken != null) {
|
||||
print('✅ Регистрация успешна, финальный токен: $finalToken');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Регистрация не удалась');
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
try {
|
||||
_isConnected = false;
|
||||
_pingTimer?.cancel();
|
||||
_socketSubscription?.cancel();
|
||||
_socket?.close();
|
||||
print('🔌 Отключено от сервера');
|
||||
} catch (e) {
|
||||
print('❌ Ошибка отключения: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,18 @@ import 'package:flutter/services.dart';
|
||||
import 'package:gwid/connection/connection_logger.dart';
|
||||
import 'package:gwid/connection/connection_state.dart' as conn_state;
|
||||
import 'package:gwid/connection/health_monitor.dart';
|
||||
import 'package:gwid/image_cache_service.dart';
|
||||
import 'package:gwid/utils/image_cache_service.dart';
|
||||
import 'package:gwid/models/complaint.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/models/message.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/utils/proxy_service.dart';
|
||||
import 'package:gwid/services/account_manager.dart';
|
||||
import 'package:gwid/services/avatar_cache_service.dart';
|
||||
import 'package:gwid/services/cache_service.dart';
|
||||
import 'package:gwid/services/chat_cache_service.dart';
|
||||
import 'package:gwid/services/profile_cache_service.dart';
|
||||
import 'package:gwid/spoofing_service.dart';
|
||||
import 'package:gwid/utils/spoofing_service.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -255,4 +255,113 @@ extension ApiServiceAuth on ApiService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Registration methods
|
||||
Future<String> startRegistration(String phoneNumber) async {
|
||||
if (_channel == null) {
|
||||
print('WebSocket не подключен, подключаемся...');
|
||||
try {
|
||||
await connect();
|
||||
await waitUntilOnline();
|
||||
} catch (e) {
|
||||
print('Ошибка подключения к WebSocket: $e');
|
||||
throw Exception('Не удалось подключиться к серверу: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final payload = {
|
||||
"phone": phoneNumber,
|
||||
"type": "START_AUTH",
|
||||
"language": "ru",
|
||||
};
|
||||
|
||||
// Listen for the response
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
final subscription = messages.listen((message) {
|
||||
if (message['opcode'] == 17 && !completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
}
|
||||
});
|
||||
|
||||
_sendMessage(17, payload);
|
||||
|
||||
try {
|
||||
final response = await completer.future.timeout(const Duration(seconds: 30));
|
||||
subscription.cancel();
|
||||
|
||||
final payload = response['payload'];
|
||||
if (payload != null && payload['token'] != null) {
|
||||
return payload['token'];
|
||||
} else {
|
||||
throw Exception('No registration token received');
|
||||
}
|
||||
} catch (e) {
|
||||
subscription.cancel();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> verifyRegistrationCode(String token, String code) async {
|
||||
final payload = {
|
||||
'token': token,
|
||||
'verifyCode': code,
|
||||
'authTokenType': 'CHECK_CODE',
|
||||
};
|
||||
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
final subscription = messages.listen((message) {
|
||||
if (message['opcode'] == 18 && !completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
}
|
||||
});
|
||||
|
||||
_sendMessage(18, payload);
|
||||
|
||||
try {
|
||||
final response = await completer.future.timeout(const Duration(seconds: 30));
|
||||
subscription.cancel();
|
||||
|
||||
final payload = response['payload'];
|
||||
if (payload != null) {
|
||||
final tokenAttrs = payload['tokenAttrs'];
|
||||
if (tokenAttrs != null && tokenAttrs['REGISTER'] != null) {
|
||||
final regToken = tokenAttrs['REGISTER']['token'];
|
||||
if (regToken != null) {
|
||||
return regToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception('Registration token not found in response');
|
||||
} catch (e) {
|
||||
subscription.cancel();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> completeRegistration(String regToken) async {
|
||||
final payload = {
|
||||
"lastName": "User",
|
||||
"token": regToken,
|
||||
"firstName": "Komet",
|
||||
"tokenType": "REGISTER",
|
||||
};
|
||||
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
final subscription = messages.listen((message) {
|
||||
if (message['opcode'] == 23 && !completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
}
|
||||
});
|
||||
|
||||
_sendMessage(23, payload);
|
||||
|
||||
try {
|
||||
await completer.future.timeout(const Duration(seconds: 30));
|
||||
subscription.cancel();
|
||||
print('Registration completed successfully');
|
||||
} catch (e) {
|
||||
subscription.cancel();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ extension ApiServiceContacts on ApiService {
|
||||
_sendMessage(34, {'contactId': contactId, 'action': 'ADD'});
|
||||
}
|
||||
|
||||
Future<void> requestContactsByIds(List<int> contactIds) async {
|
||||
await waitUntilOnline();
|
||||
_sendMessage(35, {'contactIds': contactIds});
|
||||
print('Отправлен запрос opcode=35 с contactIds: $contactIds');
|
||||
}
|
||||
|
||||
Future<void> subscribeToChat(int chatId, bool subscribe) async {
|
||||
await waitUntilOnline();
|
||||
_sendMessage(75, {'chatId': chatId, 'subscribe': subscribe});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'theme_provider.dart';
|
||||
import 'utils/theme_provider.dart';
|
||||
|
||||
class ConnectionLifecycleManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
2
lib/consts.dart
Normal file
2
lib/consts.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
// Датафайл с константами, полезно при изменении версии например
|
||||
const version = "0.3.0";
|
||||
@@ -3,9 +3,9 @@ import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'home_screen.dart';
|
||||
import 'phone_entry_screen.dart';
|
||||
import 'theme_provider.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'screens/phone_entry_screen.dart';
|
||||
import 'utils/theme_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/models/channel.dart';
|
||||
import 'package:gwid/search_channels_screen.dart';
|
||||
import 'package:gwid/screens/search_channels_screen.dart';
|
||||
|
||||
class ChannelsListScreen extends StatefulWidget {
|
||||
const ChannelsListScreen({super.key});
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
@@ -6,32 +6,31 @@ import 'package:flutter/scheduler.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
import 'package:gwid/manage_account_screen.dart';
|
||||
import 'package:gwid/screens/chat_screen.dart';
|
||||
import 'package:gwid/screens/manage_account_screen.dart';
|
||||
import 'package:gwid/screens/settings/settings_screen.dart';
|
||||
import 'package:gwid/phone_entry_screen.dart';
|
||||
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||
import 'package:gwid/models/chat.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/models/message.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/models/chat_folder.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:gwid/join_group_screen.dart';
|
||||
import 'package:gwid/search_contact_screen.dart';
|
||||
import 'package:gwid/channels_list_screen.dart';
|
||||
import 'package:gwid/screens/join_group_screen.dart';
|
||||
import 'package:gwid/screens/search_contact_screen.dart';
|
||||
import 'package:gwid/screens/channels_list_screen.dart';
|
||||
import 'package:gwid/models/channel.dart';
|
||||
import 'package:gwid/search_channels_screen.dart';
|
||||
import 'package:gwid/downloads_screen.dart';
|
||||
import 'package:gwid/user_id_lookup_screen.dart';
|
||||
import 'package:gwid/screens/search_channels_screen.dart';
|
||||
import 'package:gwid/screens/downloads_screen.dart';
|
||||
import 'package:gwid/utils/user_id_lookup_screen.dart';
|
||||
import 'package:gwid/screens/music_library_screen.dart';
|
||||
import 'package:gwid/widgets/message_preview_dialog.dart';
|
||||
import 'package:gwid/services/chat_read_settings_service.dart';
|
||||
import 'package:gwid/services/local_profile_manager.dart';
|
||||
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||
import 'package:gwid/widgets/contact_avatar_widget.dart';
|
||||
import 'package:gwid/services/contact_local_names_service.dart';
|
||||
import 'package:gwid/services/account_manager.dart';
|
||||
import 'package:gwid/models/account.dart';
|
||||
|
||||
@@ -745,7 +744,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
/*const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search, size: 20),
|
||||
onPressed: () {
|
||||
@@ -756,7 +755,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
);
|
||||
},
|
||||
tooltip: 'Поиск каналов',
|
||||
),
|
||||
),*/
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -995,7 +994,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
/*ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
@@ -1015,7 +1014,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),*/
|
||||
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
@@ -2032,7 +2031,24 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
color: colors.primary,
|
||||
size: 20,
|
||||
)
|
||||
: null,
|
||||
: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () {
|
||||
_showDeleteAccountDialog(
|
||||
context,
|
||||
account,
|
||||
accountManager,
|
||||
() {
|
||||
// Обновляем список аккаунтов
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
onTap: isCurrent
|
||||
? null
|
||||
: () async {
|
||||
@@ -3711,6 +3727,57 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _showDeleteAccountDialog(
|
||||
BuildContext context,
|
||||
Account account,
|
||||
AccountManager accountManager,
|
||||
VoidCallback onDeleted,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Удаление аккаунта'),
|
||||
content: const Text('Точно хочешь удалить аккаунт?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Нет'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
await accountManager.removeAccount(account.id);
|
||||
if (mounted) {
|
||||
onDeleted();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Аккаунт удален'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Да'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showSearchFilters() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:gwid/cache_management_screen.dart'; // Добавлен импорт
|
||||
import 'package:gwid/screens/cache_management_screen.dart'; // Добавлен импорт
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/phone_entry_screen.dart';
|
||||
import 'package:gwid/custom_request_screen.dart';
|
||||
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||
import 'package:gwid/screens/custom_request_screen.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class DebugScreen extends StatelessWidget {
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/chats_screen.dart';
|
||||
import 'package:gwid/phone_entry_screen.dart';
|
||||
import 'package:gwid/screens/chats_screen.dart';
|
||||
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/screens/settings/reconnection_screen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -10,9 +10,9 @@ import 'package:app_links/app_links.dart';
|
||||
import 'package:gwid/models/chat.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
import 'package:gwid/screens/chat_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/phone_entry_screen.dart';
|
||||
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||
import 'package:gwid/services/profile_cache_service.dart';
|
||||
import 'package:gwid/services/local_profile_manager.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/chats_screen.dart';
|
||||
import 'package:gwid/password_auth_screen.dart';
|
||||
import 'package:gwid/screens/chats_screen.dart';
|
||||
import 'package:gwid/screens/password_auth_screen.dart';
|
||||
|
||||
class OTPScreen extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/chats_screen.dart';
|
||||
import 'package:gwid/screens/chats_screen.dart';
|
||||
|
||||
class PasswordAuthScreen extends StatefulWidget {
|
||||
const PasswordAuthScreen({super.key});
|
||||
@@ -4,14 +4,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/otp_screen.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/screens/otp_screen.dart';
|
||||
import 'package:gwid/utils/proxy_service.dart';
|
||||
import 'package:gwid/screens/registration_screen.dart';
|
||||
import 'package:gwid/screens/settings/auth_settings_screen.dart';
|
||||
import 'package:gwid/token_auth_screen.dart';
|
||||
import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS
|
||||
import 'package:gwid/screens/token_auth_screen.dart';
|
||||
import 'package:gwid/screens/tos_screen.dart'; // Импорт экрана ToS
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:platform_info/platform_info.dart';
|
||||
|
||||
class Country {
|
||||
final String name;
|
||||
@@ -48,6 +50,55 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
mask: '+7 (###) ###-##-##',
|
||||
digits: 10,
|
||||
),
|
||||
Country(
|
||||
name: 'Азербайджан',
|
||||
code: '+994',
|
||||
flag: '🇦🇿',
|
||||
mask: '+994 (##) ###-##-##',
|
||||
digits: 9,
|
||||
),
|
||||
Country(
|
||||
name: 'Армения',
|
||||
code: '+374',
|
||||
flag: '🇦🇲',
|
||||
mask: '+374 (##) ###-###',
|
||||
digits: 8,
|
||||
),
|
||||
Country(
|
||||
name: 'Казахстан',
|
||||
code: '+7',
|
||||
flag: '🇰🇿',
|
||||
mask: '+7 (###) ###-##-##',
|
||||
digits: 10,
|
||||
),
|
||||
Country(
|
||||
name: 'Кыргызстан',
|
||||
code: '+996',
|
||||
flag: '🇰🇬',
|
||||
mask: '+996 (###) ###-###',
|
||||
digits: 9,
|
||||
),
|
||||
Country(
|
||||
name: 'Молдова',
|
||||
code: '+373',
|
||||
flag: '🇲🇩',
|
||||
mask: '+373 (####) ####',
|
||||
digits: 8,
|
||||
),
|
||||
Country(
|
||||
name: 'Таджикистан',
|
||||
code: '+992',
|
||||
flag: '🇹🇯',
|
||||
mask: '+992 (##) ###-##-##',
|
||||
digits: 9,
|
||||
),
|
||||
Country(
|
||||
name: 'Узбекистан',
|
||||
code: '+998',
|
||||
flag: '🇺🇿',
|
||||
mask: '+998 (##) ###-##-##',
|
||||
digits: 9,
|
||||
),
|
||||
Country(
|
||||
name: 'Беларусь',
|
||||
code: '+375',
|
||||
@@ -55,6 +106,13 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
mask: '+375 (##) ###-##-##',
|
||||
digits: 9,
|
||||
),
|
||||
Country(
|
||||
name: 'Свое',
|
||||
code: '',
|
||||
flag: '',
|
||||
mask: '',
|
||||
digits: 0, // Без ограничения
|
||||
),
|
||||
];
|
||||
|
||||
Country _selectedCountry = _countries[0];
|
||||
@@ -66,6 +124,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
StreamSubscription? _apiSubscription;
|
||||
bool _showContent = false;
|
||||
bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения
|
||||
String _customPrefix = ''; // Для "Свой префикс"
|
||||
|
||||
late final AnimationController _animationController;
|
||||
late final Animation<Alignment> _topAlignmentAnimation;
|
||||
@@ -120,8 +179,11 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
final payload = message['payload'];
|
||||
if (payload != null && payload['token'] != null) {
|
||||
final String token = payload['token'];
|
||||
final String prefix = _selectedCountry.mask.isEmpty
|
||||
? _customPrefix
|
||||
: _selectedCountry.code;
|
||||
final String fullPhoneNumber =
|
||||
_selectedCountry.code + _maskFormatter.getUnmaskedText();
|
||||
prefix + _maskFormatter.getUnmaskedText();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
@@ -141,6 +203,14 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
}
|
||||
|
||||
void _initializeMaskFormatter() {
|
||||
if (_selectedCountry.mask.isEmpty) {
|
||||
// Для "Свой префикс" - без маски, только цифры
|
||||
_maskFormatter = MaskTextInputFormatter(
|
||||
mask: '',
|
||||
filter: {"#": RegExp(r'[0-9]')},
|
||||
type: MaskAutoCompletionType.lazy,
|
||||
);
|
||||
} else {
|
||||
final mask = _selectedCountry.mask
|
||||
.replaceFirst(RegExp(r'^\+\d+\s?'), '')
|
||||
.trim();
|
||||
@@ -150,6 +220,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
type: MaskAutoCompletionType.lazy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPhoneChanged() {
|
||||
final text = _phoneController.text;
|
||||
@@ -165,8 +236,11 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
});
|
||||
}
|
||||
}
|
||||
final isFull =
|
||||
_maskFormatter.getUnmaskedText().length == _selectedCountry.digits;
|
||||
|
||||
// Для "Свой префикс" проверяем минимальную длину (например, 5 цифр)
|
||||
final isFull = _selectedCountry.mask.isEmpty
|
||||
? _maskFormatter.getUnmaskedText().length >= 5
|
||||
: _maskFormatter.getUnmaskedText().length == _selectedCountry.digits;
|
||||
if (isFull != _isButtonEnabled) {
|
||||
setState(() => _isButtonEnabled = isFull);
|
||||
}
|
||||
@@ -192,16 +266,77 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onCountryChanged(Country? country) {
|
||||
void _onCountryChanged(Country? country) async {
|
||||
if (country != null && country != _selectedCountry) {
|
||||
// Если выбран "Свой префикс", показываем диалог для ввода префикса
|
||||
if (country.mask.isEmpty) {
|
||||
final prefix = await _showCustomPrefixDialog();
|
||||
if (prefix == null || prefix.isEmpty) {
|
||||
return; // Отменено
|
||||
}
|
||||
setState(() {
|
||||
_selectedCountry = country;
|
||||
_customPrefix = prefix.startsWith('+') ? prefix : '+$prefix';
|
||||
_phoneController.clear();
|
||||
_initializeMaskFormatter();
|
||||
_isButtonEnabled = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_selectedCountry = country;
|
||||
_customPrefix = '';
|
||||
_phoneController.clear();
|
||||
_initializeMaskFormatter();
|
||||
_isButtonEnabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showCustomPrefixDialog() async {
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Введите код страны',
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.titleLarge,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.phone,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: '+123',
|
||||
prefixText: '+',
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
style: GoogleFonts.manrope(textStyle: textTheme.titleMedium),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Отмена', style: GoogleFonts.manrope()),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final prefix = controller.text.trim();
|
||||
if (prefix.isNotEmpty) {
|
||||
Navigator.of(context).pop(prefix);
|
||||
}
|
||||
},
|
||||
child: Text('OK', style: GoogleFonts.manrope()),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _checkAnonymitySettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -221,8 +356,10 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
void _requestOtp() async {
|
||||
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
|
||||
setState(() => _isLoading = true);
|
||||
final String fullPhoneNumber =
|
||||
_selectedCountry.code + _maskFormatter.getUnmaskedText();
|
||||
final String prefix = _selectedCountry.mask.isEmpty
|
||||
? _customPrefix
|
||||
: _selectedCountry.code;
|
||||
final String fullPhoneNumber = prefix + _maskFormatter.getUnmaskedText();
|
||||
try {
|
||||
ApiService.instance.errorStream.listen((error) {
|
||||
if (mounted) {
|
||||
@@ -341,7 +478,44 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
selectedCountry: _selectedCountry,
|
||||
countries: _countries,
|
||||
onCountryChanged: _onCountryChanged,
|
||||
customPrefix: _customPrefix,
|
||||
),
|
||||
|
||||
(Platform.instance.android || Platform.instance.windows) ? Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: _isTosAccepted
|
||||
? () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const RegistrationScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'зарегистрироваться',
|
||||
style: GoogleFonts.manrope(
|
||||
color: _isTosAccepted
|
||||
? colors.primary
|
||||
: colors.onSurfaceVariant.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: _isTosAccepted
|
||||
? colors.primary
|
||||
: colors.onSurfaceVariant.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)]
|
||||
) : const SizedBox(),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -517,6 +691,7 @@ class _PhoneInput extends StatelessWidget {
|
||||
final Country selectedCountry;
|
||||
final List<Country> countries;
|
||||
final ValueChanged<Country?> onCountryChanged;
|
||||
final String customPrefix;
|
||||
|
||||
const _PhoneInput({
|
||||
required this.phoneController,
|
||||
@@ -524,6 +699,7 @@ class _PhoneInput extends StatelessWidget {
|
||||
required this.selectedCountry,
|
||||
required this.countries,
|
||||
required this.onCountryChanged,
|
||||
required this.customPrefix,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -542,6 +718,7 @@ class _PhoneInput extends StatelessWidget {
|
||||
selectedCountry: selectedCountry,
|
||||
countries: countries,
|
||||
onCountryChanged: onCountryChanged,
|
||||
customPrefix: customPrefix,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
@@ -556,11 +733,13 @@ class _CountryPicker extends StatelessWidget {
|
||||
final Country selectedCountry;
|
||||
final List<Country> countries;
|
||||
final ValueChanged<Country?> onCountryChanged;
|
||||
final String customPrefix;
|
||||
|
||||
const _CountryPicker({
|
||||
required this.selectedCountry,
|
||||
required this.countries,
|
||||
required this.onCountryChanged,
|
||||
required this.customPrefix,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -575,16 +754,40 @@ class _CountryPicker extends StatelessWidget {
|
||||
value: selectedCountry,
|
||||
onChanged: onCountryChanged,
|
||||
icon: Icon(Icons.keyboard_arrow_down, color: colors.onSurfaceVariant),
|
||||
selectedItemBuilder: (BuildContext context) {
|
||||
return countries.map<Widget>((Country country) {
|
||||
final displayText = country.mask.isEmpty
|
||||
? (customPrefix.isNotEmpty ? customPrefix : country.name)
|
||||
: country.code;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
displayText,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.titleMedium,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
items: countries.map((Country country) {
|
||||
return DropdownMenuItem<Country>(
|
||||
value: country,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (country.flag.isNotEmpty) ...[
|
||||
Text(country.flag, style: textTheme.titleMedium),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Text(
|
||||
country.code,
|
||||
country.code.isEmpty ? 'Свое' : country.code,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.titleMedium,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/manage_account_screen.dart';
|
||||
import 'package:gwid/screens/manage_account_screen.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/screens/settings/settings_screen.dart';
|
||||
import 'package:gwid/phone_entry_screen.dart';
|
||||
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
|
||||
class ProfileMenuDialog extends StatefulWidget {
|
||||
final Profile? myProfile;
|
||||
542
lib/screens/registration_screen.dart
Normal file
542
lib/screens/registration_screen.dart
Normal file
@@ -0,0 +1,542 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:gwid/api/api_registration_service.dart';
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
|
||||
class Country {
|
||||
final String name;
|
||||
final String code;
|
||||
final String flag;
|
||||
final String mask;
|
||||
final int digits;
|
||||
|
||||
const Country({
|
||||
required this.name,
|
||||
required this.code,
|
||||
required this.flag,
|
||||
required this.mask,
|
||||
required this.digits,
|
||||
});
|
||||
}
|
||||
|
||||
class RegistrationScreen extends StatefulWidget {
|
||||
const RegistrationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegistrationScreen> createState() => _RegistrationScreenState();
|
||||
}
|
||||
|
||||
class _RegistrationScreenState extends State<RegistrationScreen>
|
||||
with TickerProviderStateMixin {
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
final TextEditingController _codeController = TextEditingController();
|
||||
|
||||
static const List<Country> _countries = [
|
||||
Country(
|
||||
name: 'Россия',
|
||||
code: '+7',
|
||||
flag: '🇷🇺',
|
||||
mask: '+7 (###) ###-##-##',
|
||||
digits: 10,
|
||||
),
|
||||
Country(
|
||||
name: 'Беларусь',
|
||||
code: '+375',
|
||||
flag: '🇧🇾',
|
||||
mask: '+375 (##) ###-##-##',
|
||||
digits: 9,
|
||||
),
|
||||
];
|
||||
|
||||
Country _selectedCountry = _countries[0];
|
||||
late MaskTextInputFormatter _maskFormatter;
|
||||
bool _isButtonEnabled = false;
|
||||
bool _isLoading = false;
|
||||
bool _showCodeInput = false;
|
||||
bool _showContent = false;
|
||||
String? _registrationToken;
|
||||
final RegistrationService _registrationService = RegistrationService();
|
||||
|
||||
late final AnimationController _animationController;
|
||||
late final Animation<Alignment> _topAlignmentAnimation;
|
||||
late final Animation<Alignment> _bottomAlignmentAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print('🎬 RegistrationScreen инициализирован');
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 15),
|
||||
);
|
||||
|
||||
_topAlignmentAnimation =
|
||||
AlignmentTween(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.topRight,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
_bottomAlignmentAnimation =
|
||||
AlignmentTween(
|
||||
begin: Alignment.bottomRight,
|
||||
end: Alignment.bottomLeft,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
_animationController.repeat(reverse: true);
|
||||
|
||||
_initializeMaskFormatter();
|
||||
_phoneController.addListener(_onPhoneChanged);
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (mounted) setState(() => _showContent = true);
|
||||
});
|
||||
}
|
||||
|
||||
void _initializeMaskFormatter() {
|
||||
final mask = _selectedCountry.mask
|
||||
.replaceFirst(RegExp(r'^\+\d+\s?'), '')
|
||||
.trim();
|
||||
_maskFormatter = MaskTextInputFormatter(
|
||||
mask: mask,
|
||||
filter: {"#": RegExp(r'[0-9]')},
|
||||
type: MaskAutoCompletionType.lazy,
|
||||
);
|
||||
}
|
||||
|
||||
void _onPhoneChanged() {
|
||||
final text = _phoneController.text;
|
||||
if (text.isNotEmpty) {
|
||||
Country? detectedCountry = _detectCountryFromInput(text);
|
||||
if (detectedCountry != null && detectedCountry != _selectedCountry) {
|
||||
if (_shouldClearFieldForCountry(text, detectedCountry)) {
|
||||
_phoneController.clear();
|
||||
}
|
||||
setState(() {
|
||||
_selectedCountry = detectedCountry;
|
||||
_initializeMaskFormatter();
|
||||
});
|
||||
}
|
||||
}
|
||||
final isFull =
|
||||
_maskFormatter.getUnmaskedText().length == _selectedCountry.digits;
|
||||
if (isFull != _isButtonEnabled) {
|
||||
setState(() => _isButtonEnabled = isFull);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldClearFieldForCountry(String input, Country country) {
|
||||
final cleanInput = input.replaceAll(RegExp(r'[^\d+]'), '');
|
||||
if (country.code == '+7') {
|
||||
return !(cleanInput.startsWith('+7') || cleanInput.startsWith('7'));
|
||||
} else if (country.code == '+375') {
|
||||
return !(cleanInput.startsWith('+375') || cleanInput.startsWith('375'));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Country? _detectCountryFromInput(String input) {
|
||||
final cleanInput = input.replaceAll(RegExp(r'[^\d+]'), '');
|
||||
if (cleanInput.startsWith('+7') || cleanInput.startsWith('7')) {
|
||||
return _countries.firstWhere((c) => c.code == '+7');
|
||||
} else if (cleanInput.startsWith('+375') || cleanInput.startsWith('375')) {
|
||||
return _countries.firstWhere((c) => c.code == '+375');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onCountryChanged(Country? country) {
|
||||
if (country != null && country != _selectedCountry) {
|
||||
setState(() {
|
||||
_selectedCountry = country;
|
||||
_phoneController.clear();
|
||||
_initializeMaskFormatter();
|
||||
_isButtonEnabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startRegistration() async {
|
||||
if (!_isButtonEnabled || _isLoading) return;
|
||||
|
||||
print('🔄 Начинаем процесс регистрации...');
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final fullPhoneNumber =
|
||||
_selectedCountry.code + _maskFormatter.getUnmaskedText();
|
||||
print('📞 Номер телефона: $fullPhoneNumber');
|
||||
|
||||
// Запускаем процесс регистрации
|
||||
final token = await _registrationService.startRegistration(
|
||||
fullPhoneNumber,
|
||||
);
|
||||
print('✅ Токен получен: ${token.substring(0, 20)}...');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_showCodeInput = true;
|
||||
_registrationToken = token;
|
||||
});
|
||||
print('✅ Переходим к вводу кода');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Ошибка в процессе регистрации: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка регистрации: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyRegistrationCode(String code) async {
|
||||
if (_registrationToken == null || _isLoading) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
print('🔐 Код подтверждения: $code');
|
||||
|
||||
// Проверяем код и получаем токен регистрации
|
||||
final registerToken = await _registrationService.verifyCode(
|
||||
_registrationToken!,
|
||||
code,
|
||||
);
|
||||
|
||||
// Завершаем регистрацию
|
||||
await _registrationService.completeRegistration(registerToken);
|
||||
|
||||
print('✅ Регистрация завершена успешно!');
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Регистрация завершена успешно!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Ошибка при завершении регистрации: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
|
||||
// Проверяем, существует ли уже аккаунт
|
||||
if (e.toString().contains('ACCOUNT_EXISTS')) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'А зачем... Аккаунт на таком номере уже существует!',
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
// Закрываем экран регистрации
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: _topAlignmentAnimation.value,
|
||||
end: _bottomAlignmentAnimation.value,
|
||||
colors: [
|
||||
Color.lerp(colors.surface, colors.primary, 0.2)!,
|
||||
Color.lerp(colors.surface, colors.tertiary, 0.15)!,
|
||||
colors.surface,
|
||||
Color.lerp(colors.surface, colors.secondary, 0.15)!,
|
||||
Color.lerp(colors.surface, colors.primary, 0.25)!,
|
||||
],
|
||||
stops: const [0.0, 0.25, 0.5, 0.75, 1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 340),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 700),
|
||||
curve: Curves.easeOut,
|
||||
opacity: _showContent ? 1.0 : 0.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 48),
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colors.primary.withOpacity(0.1),
|
||||
),
|
||||
child: const Image(
|
||||
image: AssetImage(
|
||||
'assets/images/komet_512.png',
|
||||
),
|
||||
width: 75,
|
||||
height: 75,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Модуль регистрации',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.headlineMedium,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
if (!_showCodeInput) ...[
|
||||
_PhoneInput(
|
||||
phoneController: _phoneController,
|
||||
maskFormatter: _maskFormatter,
|
||||
selectedCountry: _selectedCountry,
|
||||
countries: _countries,
|
||||
onCountryChanged: _onCountryChanged,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: _isButtonEnabled && !_isLoading
|
||||
? _startRegistration
|
||||
: null,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Отправить код',
|
||||
style: GoogleFonts.manrope(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
'Введите код подтверждения',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.titleMedium,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineMedium,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '000000',
|
||||
counterText: '',
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value.length == 6) {
|
||||
_verifyRegistrationCode(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'Назад',
|
||||
style: GoogleFonts.manrope(
|
||||
color: colors.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isLoading)
|
||||
Container(
|
||||
color: colors.scrim.withOpacity(0.7),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
colors.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_showCodeInput ? 'Регистрируем...' : 'Отправляем код...',
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
color: colors.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_phoneController.dispose();
|
||||
_codeController.dispose();
|
||||
_registrationService.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _PhoneInput extends StatelessWidget {
|
||||
final TextEditingController phoneController;
|
||||
final MaskTextInputFormatter maskFormatter;
|
||||
final Country selectedCountry;
|
||||
final List<Country> countries;
|
||||
final ValueChanged<Country?> onCountryChanged;
|
||||
|
||||
const _PhoneInput({
|
||||
required this.phoneController,
|
||||
required this.maskFormatter,
|
||||
required this.selectedCountry,
|
||||
required this.countries,
|
||||
required this.onCountryChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: phoneController,
|
||||
inputFormatters: [maskFormatter],
|
||||
keyboardType: TextInputType.number,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: Theme.of(context).textTheme.titleMedium,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: maskFormatter.getMask()?.replaceAll('#', '0'),
|
||||
prefixIcon: _CountryPicker(
|
||||
selectedCountry: selectedCountry,
|
||||
countries: countries,
|
||||
onCountryChanged: onCountryChanged,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountryPicker extends StatelessWidget {
|
||||
final Country selectedCountry;
|
||||
final List<Country> countries;
|
||||
final ValueChanged<Country?> onCountryChanged;
|
||||
|
||||
const _CountryPicker({
|
||||
required this.selectedCountry,
|
||||
required this.countries,
|
||||
required this.onCountryChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Country>(
|
||||
value: selectedCountry,
|
||||
onChanged: onCountryChanged,
|
||||
icon: Icon(Icons.keyboard_arrow_down, color: colors.onSurfaceVariant),
|
||||
items: countries.map((Country country) {
|
||||
return DropdownMenuItem<Country>(
|
||||
value: country,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(country.flag, style: textTheme.titleMedium),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
country.code,
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.titleMedium,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/screens/chat_screen.dart';
|
||||
|
||||
class SearchContactScreen extends StatefulWidget {
|
||||
const SearchContactScreen({super.key});
|
||||
@@ -36,7 +35,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
_apiSubscription = ApiService.instance.messages.listen((message) {
|
||||
if (!mounted) return;
|
||||
|
||||
|
||||
if (message['type'] == 'contact_found') {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
@@ -48,8 +46,10 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
|
||||
if (contactData != null) {
|
||||
_foundContact = Contact.fromJson(contactData);
|
||||
}
|
||||
|
||||
// Автоматически открываем чат с найденным контактом
|
||||
_openChatWithContact(_foundContact!);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Контакт найден!'),
|
||||
@@ -62,7 +62,7 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (message['type'] == 'contact_not_found') {
|
||||
setState(() {
|
||||
@@ -118,7 +118,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!phone.startsWith('+') || phone.length < 10) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -160,13 +159,21 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _startChat() {
|
||||
if (_foundContact != null) {
|
||||
Future<void> _openChatWithContact(Contact contact) async {
|
||||
try {
|
||||
print(
|
||||
'🔍 Открываем чат с контактом: ${contact.name} (ID: ${contact.id})',
|
||||
);
|
||||
|
||||
// Получаем chatId по contactId
|
||||
final chatId = await ApiService.instance.getChatIdByUserId(contact.id);
|
||||
|
||||
if (chatId == null) {
|
||||
print('⚠️ Чат не найден для контакта ${contact.id}');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Создание чата с ${_foundContact!.name}'),
|
||||
backgroundColor: Colors.blue,
|
||||
content: const Text('Не удалось найти чат с этим контактом'),
|
||||
backgroundColor: Colors.orange,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -174,6 +181,124 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
margin: const EdgeInsets.all(10),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
print('✅ Найден chatId: $chatId');
|
||||
|
||||
// Подписываемся на чат
|
||||
await ApiService.instance.subscribeToChat(chatId, true);
|
||||
print('✅ Подписались на чат $chatId');
|
||||
|
||||
// Получаем myId из профиля
|
||||
final profileData = ApiService.instance.lastChatsPayload?['profile'];
|
||||
final contactProfile = profileData?['contact'] as Map<String, dynamic>?;
|
||||
final myId = contactProfile?['id'] as int? ?? 0;
|
||||
|
||||
if (myId == 0) {
|
||||
print('⚠️ Не удалось получить myId, используем 0');
|
||||
}
|
||||
|
||||
// Открываем ChatScreen
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
chatId: chatId,
|
||||
contact: contact,
|
||||
myId: myId,
|
||||
isGroupChat: false,
|
||||
isChannel: false,
|
||||
onChatUpdated: () {
|
||||
print('Chat updated');
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Ошибка при открытии чата: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка при открытии чата: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(10),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startChat() {
|
||||
if (_foundContact != null) {
|
||||
_openChatWithContact(_foundContact!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startChatAlternative() async {
|
||||
if (_foundContact == null) return;
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
print('🔄 Альтернативный способ: добавляем контакт ${_foundContact!.id}');
|
||||
|
||||
// Отправляем opcode=34 с action="ADD"
|
||||
await ApiService.instance.addContact(_foundContact!.id);
|
||||
print('✅ Отправлен opcode=34 с action=ADD');
|
||||
|
||||
// Отправляем opcode=35 с contactIds
|
||||
await ApiService.instance.requestContactsByIds([_foundContact!.id]);
|
||||
print('✅ Отправлен opcode=35 с contactIds=[${_foundContact!.id}]');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Показываем диалог о необходимости перезайти
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Перезайти в приложение'),
|
||||
content: const Text(
|
||||
'Для завершения добавления контакта необходимо перезайти в приложение.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Понятно'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Ошибка при альтернативном способе: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(10),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +319,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
@@ -230,7 +354,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
Text(
|
||||
'Номер телефона',
|
||||
style: Theme.of(
|
||||
@@ -322,7 +445,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
if (_foundContact != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
@@ -411,12 +533,27 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _startChatAlternative,
|
||||
icon: const Icon(Icons.alternate_email),
|
||||
label: const Text(
|
||||
'Начать чат альтернативным способом',
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/tos_screen.dart';
|
||||
import 'package:gwid/screens/tos_screen.dart';
|
||||
import 'package:gwid/consts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
@@ -239,7 +240,7 @@ class AboutScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Версия 0.3.0',
|
||||
'Версия $version',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: colors.onSurface.withOpacity(0.7),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
|
||||
|
||||
class AnimationsScreen extends StatelessWidget {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:gwid/screens/settings/customization_screen.dart';
|
||||
import 'package:gwid/screens/settings/animations_screen.dart';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/utils/proxy_service.dart';
|
||||
import 'package:gwid/screens/settings/proxy_settings_screen.dart';
|
||||
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
|
||||
class BypassScreen extends StatelessWidget {
|
||||
final bool isModal;
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:gwid/models/message.dart';
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/spoofing_service.dart';
|
||||
import 'package:gwid/utils/proxy_service.dart';
|
||||
import 'package:gwid/utils/spoofing_service.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:gwid/password_management_screen.dart';
|
||||
import 'package:gwid/screens/password_management_screen.dart';
|
||||
|
||||
class PrivacySettingsScreen extends StatefulWidget {
|
||||
const PrivacySettingsScreen({super.key});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/proxy_settings.dart';
|
||||
import 'package:gwid/utils/proxy_service.dart';
|
||||
import 'package:gwid/utils/proxy_settings.dart';
|
||||
|
||||
class ProxySettingsScreen extends StatefulWidget {
|
||||
const ProxySettingsScreen({super.key});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/home_screen.dart';
|
||||
import 'package:gwid/screens/home_screen.dart';
|
||||
|
||||
class ReconnectionScreen extends StatefulWidget {
|
||||
const ReconnectionScreen({super.key});
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:flutter_timezone/flutter_timezone.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:gwid/device_presets.dart';
|
||||
import 'package:gwid/utils/device_presets.dart';
|
||||
|
||||
enum SpoofingMethod { partial, full }
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/consts.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/manage_account_screen.dart';
|
||||
import 'package:gwid/screens/manage_account_screen.dart';
|
||||
import 'package:gwid/screens/settings/appearance_settings_screen.dart';
|
||||
import 'package:gwid/screens/settings/notification_settings_screen.dart';
|
||||
import 'package:gwid/screens/settings/privacy_security_screen.dart';
|
||||
@@ -9,9 +10,9 @@ import 'package:gwid/screens/settings/storage_screen.dart';
|
||||
import 'package:gwid/screens/settings/network_settings_screen.dart';
|
||||
import 'package:gwid/screens/settings/bypass_screen.dart';
|
||||
import 'package:gwid/screens/settings/about_screen.dart';
|
||||
import 'package:gwid/debug_screen.dart';
|
||||
import 'package:gwid/screens/debug_screen.dart';
|
||||
import 'package:gwid/screens/settings/komet_misc_screen.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
@@ -208,7 +209,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: _buildSettingsContent(),
|
||||
body: SafeArea(
|
||||
child: _buildSettingsContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -468,7 +471,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Text(
|
||||
'v0.3.0-beta.1',
|
||||
version,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
|
||||
@@ -11,9 +11,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/home_screen.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/proxy_settings.dart';
|
||||
import 'package:gwid/screens/home_screen.dart';
|
||||
import 'package:gwid/utils/proxy_service.dart';
|
||||
import 'package:gwid/utils/proxy_settings.dart';
|
||||
import 'package:gwid/screens/settings/qr_scanner_screen.dart';
|
||||
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
||||
|
||||
@@ -9,26 +9,30 @@ import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:gwid/models/message.dart';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/theme_provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
import 'package:gwid/screens/chat_screen.dart';
|
||||
import 'package:gwid/services/avatar_cache_service.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'dart:async';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:gwid/full_screen_video_player.dart';
|
||||
import 'package:gwid/widgets/full_screen_video_player.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:gwid/services/cache_service.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:gwid/services/music_player_service.dart';
|
||||
import 'package:platform_info/platform_info.dart';
|
||||
|
||||
bool _currentIsDark = false;
|
||||
|
||||
bool isMobile = Platform.instance.operatingSystem.iOS ||
|
||||
Platform.instance.operatingSystem.android;
|
||||
|
||||
enum MessageReadStatus {
|
||||
sending, // Отправляется (часы)
|
||||
sent, // Отправлено (1 галочка)
|
||||
@@ -1081,12 +1085,22 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||
if (isMobile) {
|
||||
messageContent = GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: messageContent,
|
||||
);
|
||||
} else {
|
||||
messageContent = GestureDetector(
|
||||
onSecondaryTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: messageContent,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Column(
|
||||
@@ -1535,12 +1549,21 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||
if (isMobile) {
|
||||
videoContent = GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: videoContent,
|
||||
);
|
||||
} else {
|
||||
videoContent = GestureDetector(
|
||||
onSecondaryTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: videoContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return videoContent;
|
||||
@@ -1621,12 +1644,21 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||
if (isMobile) {
|
||||
photoContent = GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: photoContent,
|
||||
);
|
||||
} else {
|
||||
photoContent = GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: photoContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return photoContent;
|
||||
@@ -1749,12 +1781,21 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||
if (isMobile) {
|
||||
videoContent = GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: videoContent,
|
||||
);
|
||||
} else {
|
||||
videoContent = GestureDetector(
|
||||
onSecondaryTapDown: (TapDownDetails details) {
|
||||
_showMessageContextMenu(context, details.globalPosition);
|
||||
},
|
||||
child: videoContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return videoContent;
|
||||
|
||||
935
lib/widgets/full_screen_video_player.dart
Normal file
935
lib/widgets/full_screen_video_player.dart
Normal file
@@ -0,0 +1,935 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class FullScreenVideoPlayer extends StatefulWidget {
|
||||
final String videoUrl;
|
||||
|
||||
const FullScreenVideoPlayer({Key? key, required this.videoUrl})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<FullScreenVideoPlayer> createState() => _FullScreenVideoPlayerState();
|
||||
}
|
||||
|
||||
class _FullScreenVideoPlayerState extends State<FullScreenVideoPlayer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
VideoPlayerController? _videoPlayerController;
|
||||
bool _isLoading = true;
|
||||
bool _hasError = false;
|
||||
bool _isPlaying = false;
|
||||
bool _showControls = true;
|
||||
bool _isBuffering = false;
|
||||
double _playbackSpeed = 1.0;
|
||||
Timer? _hideControlsTimer;
|
||||
Timer? _positionTimer;
|
||||
late AnimationController _controlsAnimationController;
|
||||
late Animation<double> _controlsAnimation;
|
||||
bool _isDragging = false;
|
||||
Duration _currentPosition = Duration.zero;
|
||||
Duration _totalDuration = Duration.zero;
|
||||
List<DurationRange> _bufferedRanges = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
|
||||
_controlsAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
_controlsAnimation = CurvedAnimation(
|
||||
parent: _controlsAnimationController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
_controlsAnimationController.forward();
|
||||
_initializePlayer();
|
||||
}
|
||||
|
||||
Future<void> _initializePlayer() async {
|
||||
try {
|
||||
_videoPlayerController = VideoPlayerController.networkUrl(
|
||||
Uri.parse(widget.videoUrl),
|
||||
httpHeaders: const {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
},
|
||||
);
|
||||
|
||||
_videoPlayerController!.addListener(_videoListener);
|
||||
await _videoPlayerController!.initialize();
|
||||
_videoPlayerController!.play();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isPlaying = true;
|
||||
_totalDuration = _videoPlayerController!.value.duration;
|
||||
_currentPosition = _videoPlayerController!.value.position;
|
||||
});
|
||||
_startHideControlsTimer();
|
||||
_startPositionTimer();
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ [FullScreenVideoPlayer] Error initializing player: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _videoListener() {
|
||||
if (!mounted) return;
|
||||
|
||||
final controller = _videoPlayerController!;
|
||||
setState(() {
|
||||
_isPlaying = controller.value.isPlaying;
|
||||
_isBuffering = controller.value.isBuffering;
|
||||
_totalDuration = controller.value.duration;
|
||||
_bufferedRanges = controller.value.buffered;
|
||||
if (!_isDragging) {
|
||||
_currentPosition = controller.value.position;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startHideControlsTimer() {
|
||||
_hideControlsTimer?.cancel();
|
||||
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (_isPlaying && !_isDragging) {
|
||||
_hideControlsUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startPositionTimer() {
|
||||
_positionTimer?.cancel();
|
||||
_positionTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||
if (!mounted || _isDragging) return;
|
||||
if (_videoPlayerController != null &&
|
||||
_videoPlayerController!.value.isInitialized) {
|
||||
setState(() {
|
||||
_currentPosition = _videoPlayerController!.value.position;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showControlsUI() {
|
||||
if (_showControls) return;
|
||||
setState(() {
|
||||
_showControls = true;
|
||||
});
|
||||
_controlsAnimationController.forward();
|
||||
_startHideControlsTimer();
|
||||
}
|
||||
|
||||
void _hideControlsUI() {
|
||||
if (!_showControls) return;
|
||||
setState(() {
|
||||
_showControls = false;
|
||||
});
|
||||
_controlsAnimationController.reverse();
|
||||
}
|
||||
|
||||
void _togglePlayPause() {
|
||||
setState(() {
|
||||
if (_isPlaying) {
|
||||
_videoPlayerController!.pause();
|
||||
_showControlsUI();
|
||||
} else {
|
||||
_videoPlayerController!.play();
|
||||
_startHideControlsTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Duration _clampDuration(Duration value, Duration min, Duration max) {
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
void _seekTo(Duration position) {
|
||||
_videoPlayerController!.seekTo(position);
|
||||
setState(() {
|
||||
_currentPosition = position;
|
||||
_isDragging = false;
|
||||
});
|
||||
_startHideControlsTimer();
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
|
||||
if (hours > 0) {
|
||||
return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}';
|
||||
}
|
||||
return '${twoDigits(minutes)}:${twoDigits(seconds)}';
|
||||
}
|
||||
|
||||
void _showSpeedMenu() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _SpeedBottomSheet(
|
||||
currentSpeed: _playbackSpeed,
|
||||
onSpeedSelected: (speed) {
|
||||
setState(() {
|
||||
_playbackSpeed = speed;
|
||||
_videoPlayerController!.setPlaybackSpeed(speed);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
_showControlsUI();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideControlsTimer?.cancel();
|
||||
_positionTimer?.cancel();
|
||||
_videoPlayerController?.removeListener(_videoListener);
|
||||
_videoPlayerController?.dispose();
|
||||
_controlsAnimationController.dispose();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
if (_showControls) {
|
||||
_hideControlsUI();
|
||||
} else {
|
||||
_showControlsUI();
|
||||
}
|
||||
},
|
||||
onDoubleTapDown: (details) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (details.globalPosition.dx < screenWidth / 2) {
|
||||
final newPosition = _clampDuration(
|
||||
_currentPosition - const Duration(seconds: 10),
|
||||
Duration.zero,
|
||||
_totalDuration,
|
||||
);
|
||||
_seekTo(newPosition);
|
||||
_showControlsUI();
|
||||
} else {
|
||||
final newPosition = _clampDuration(
|
||||
_currentPosition + const Duration(seconds: 10),
|
||||
Duration.zero,
|
||||
_totalDuration,
|
||||
);
|
||||
_seekTo(newPosition);
|
||||
_showControlsUI();
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: _isLoading
|
||||
? CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
)
|
||||
: _hasError
|
||||
? _ErrorWidget(colorScheme: colorScheme)
|
||||
: _videoPlayerController != null &&
|
||||
_videoPlayerController!.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _videoPlayerController!.value.aspectRatio,
|
||||
child: VideoPlayer(_videoPlayerController!),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
|
||||
if (_isBuffering)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Буферизация...',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_showControls)
|
||||
GestureDetector(
|
||||
onDoubleTapDown: (details) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (details.globalPosition.dx < screenWidth / 2) {
|
||||
final newPosition = _clampDuration(
|
||||
_currentPosition - const Duration(seconds: 10),
|
||||
Duration.zero,
|
||||
_totalDuration,
|
||||
);
|
||||
_seekTo(newPosition);
|
||||
_showControlsUI();
|
||||
} else {
|
||||
final newPosition = _clampDuration(
|
||||
_currentPosition + const Duration(seconds: 10),
|
||||
Duration.zero,
|
||||
_totalDuration,
|
||||
);
|
||||
_seekTo(newPosition);
|
||||
_showControlsUI();
|
||||
}
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controlsAnimation,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _controlsAnimation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _VideoControls(
|
||||
colorScheme: colorScheme,
|
||||
isPlaying: _isPlaying,
|
||||
currentPosition: _currentPosition,
|
||||
totalDuration: _totalDuration,
|
||||
bufferedRanges: _bufferedRanges,
|
||||
playbackSpeed: _playbackSpeed,
|
||||
onPlayPause: _togglePlayPause,
|
||||
onSeek: (position) {
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
_currentPosition = position;
|
||||
});
|
||||
},
|
||||
onSeekEnd: (position) {
|
||||
_seekTo(position);
|
||||
},
|
||||
onBack: () => Navigator.pop(context),
|
||||
onSpeedTap: () {
|
||||
_showSpeedMenu();
|
||||
},
|
||||
onRewind: () {
|
||||
final newPosition = _clampDuration(
|
||||
_currentPosition - const Duration(seconds: 10),
|
||||
Duration.zero,
|
||||
_totalDuration,
|
||||
);
|
||||
_seekTo(newPosition);
|
||||
_showControlsUI();
|
||||
},
|
||||
onForward: () {
|
||||
final newPosition = _clampDuration(
|
||||
_currentPosition + const Duration(seconds: 10),
|
||||
Duration.zero,
|
||||
_totalDuration,
|
||||
);
|
||||
_seekTo(newPosition);
|
||||
_showControlsUI();
|
||||
},
|
||||
formatDuration: _formatDuration,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoControls extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final bool isPlaying;
|
||||
final Duration currentPosition;
|
||||
final Duration totalDuration;
|
||||
final List<DurationRange> bufferedRanges;
|
||||
final double playbackSpeed;
|
||||
final VoidCallback onPlayPause;
|
||||
final Function(Duration) onSeek;
|
||||
final Function(Duration) onSeekEnd;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onSpeedTap;
|
||||
final VoidCallback onRewind;
|
||||
final VoidCallback onForward;
|
||||
final String Function(Duration) formatDuration;
|
||||
|
||||
const _VideoControls({
|
||||
required this.colorScheme,
|
||||
required this.isPlaying,
|
||||
required this.currentPosition,
|
||||
required this.totalDuration,
|
||||
required this.bufferedRanges,
|
||||
required this.playbackSpeed,
|
||||
required this.onPlayPause,
|
||||
required this.onSeek,
|
||||
required this.onSeekEnd,
|
||||
required this.onBack,
|
||||
required this.onSpeedTap,
|
||||
required this.onRewind,
|
||||
required this.onForward,
|
||||
required this.formatDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progress = totalDuration.inMilliseconds > 0
|
||||
? currentPosition.inMilliseconds / totalDuration.inMilliseconds
|
||||
: 0.0;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.7),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onBack,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black.withOpacity(0.5),
|
||||
foregroundColor: Colors.white,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.tonal(
|
||||
onPressed: onSpeedTap,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.black.withOpacity(0.5),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.speed, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${playbackSpeed}x',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Прогресс-бар
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
_CustomProgressBar(
|
||||
progress: progress,
|
||||
currentPosition: currentPosition,
|
||||
totalDuration: totalDuration,
|
||||
bufferedRanges: bufferedRanges,
|
||||
onSeek: onSeek,
|
||||
onSeekEnd: onSeekEnd,
|
||||
colorScheme: colorScheme,
|
||||
formatDuration: formatDuration,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
formatDuration(currentPosition),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
' / ',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatDuration(totalDuration),
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_MaterialYouControlButton(
|
||||
icon: Icons.replay_10,
|
||||
onTap: onRewind,
|
||||
colorScheme: colorScheme,
|
||||
label: '-10',
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_MaterialYouControlButton(
|
||||
icon: isPlaying ? Icons.pause : Icons.play_arrow,
|
||||
onTap: onPlayPause,
|
||||
colorScheme: colorScheme,
|
||||
isPrimary: true,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Кнопка перемотки вперед
|
||||
_MaterialYouControlButton(
|
||||
icon: Icons.forward_10,
|
||||
onTap: onForward,
|
||||
colorScheme: colorScheme,
|
||||
label: '+10',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialYouControlButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final ColorScheme colorScheme;
|
||||
final String? label;
|
||||
final bool isPrimary;
|
||||
|
||||
const _MaterialYouControlButton({
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
required this.colorScheme,
|
||||
this.label,
|
||||
this.isPrimary = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isPrimary) {
|
||||
return FilledButton(
|
||||
onPressed: onTap,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.all(20),
|
||||
shape: const CircleBorder(),
|
||||
minimumSize: const Size(72, 72),
|
||||
elevation: 3,
|
||||
),
|
||||
child: Icon(icon, size: 36),
|
||||
);
|
||||
} else {
|
||||
return FilledButton.tonal(
|
||||
onPressed: onTap,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white.withOpacity(0.16),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.all(14),
|
||||
shape: const CircleBorder(),
|
||||
minimumSize: const Size(60, 60),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 26),
|
||||
if (label != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label!,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomProgressBar extends StatefulWidget {
|
||||
final double progress;
|
||||
final Duration currentPosition;
|
||||
final Duration totalDuration;
|
||||
final List<DurationRange> bufferedRanges;
|
||||
final Function(Duration) onSeek;
|
||||
final Function(Duration) onSeekEnd;
|
||||
final ColorScheme colorScheme;
|
||||
final String Function(Duration) formatDuration;
|
||||
|
||||
const _CustomProgressBar({
|
||||
required this.progress,
|
||||
required this.currentPosition,
|
||||
required this.totalDuration,
|
||||
required this.bufferedRanges,
|
||||
required this.onSeek,
|
||||
required this.onSeekEnd,
|
||||
required this.colorScheme,
|
||||
required this.formatDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CustomProgressBar> createState() => _CustomProgressBarState();
|
||||
}
|
||||
|
||||
class _CustomProgressBarState extends State<_CustomProgressBar> {
|
||||
bool _isDragging = false;
|
||||
double _dragProgress = 0.0;
|
||||
|
||||
Duration _getPositionFromLocalPosition(Offset localPosition, Size size) {
|
||||
final progress = (localPosition.dx / size.width).clamp(0.0, 1.0);
|
||||
return Duration(
|
||||
milliseconds: (progress * widget.totalDuration.inMilliseconds).round(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progress = _isDragging ? _dragProgress : widget.progress;
|
||||
final currentPos = Duration(
|
||||
milliseconds: (progress * widget.totalDuration.inMilliseconds).round(),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onPanStart: (details) {
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
});
|
||||
final box = context.findRenderObject() as RenderBox;
|
||||
final localPosition = box.globalToLocal(details.globalPosition);
|
||||
_dragProgress = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
|
||||
final position = _getPositionFromLocalPosition(localPosition, box.size);
|
||||
widget.onSeek(position);
|
||||
},
|
||||
onPanUpdate: (details) {
|
||||
final box = context.findRenderObject() as RenderBox;
|
||||
final localPosition = box.globalToLocal(details.globalPosition);
|
||||
setState(() {
|
||||
_dragProgress = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
|
||||
});
|
||||
final position = _getPositionFromLocalPosition(localPosition, box.size);
|
||||
widget.onSeek(position);
|
||||
},
|
||||
onPanEnd: (details) {
|
||||
setState(() {
|
||||
_isDragging = false;
|
||||
});
|
||||
widget.onSeekEnd(currentPos);
|
||||
},
|
||||
onTapDown: (details) {
|
||||
if (_isDragging) return;
|
||||
final box = context.findRenderObject() as RenderBox;
|
||||
final localPosition = box.globalToLocal(details.globalPosition);
|
||||
final position = _getPositionFromLocalPosition(localPosition, box.size);
|
||||
widget.onSeekEnd(position);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final containerWidth = constraints.maxWidth;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.totalDuration.inMilliseconds > 0)
|
||||
...widget.bufferedRanges.map((range) {
|
||||
final startProgress = (range.start.inMilliseconds / widget.totalDuration.inMilliseconds).clamp(0.0, 1.0);
|
||||
final endProgress = (range.end.inMilliseconds / widget.totalDuration.inMilliseconds).clamp(0.0, 1.0);
|
||||
final bufferedWidth = (endProgress - startProgress).clamp(0.0, 1.0);
|
||||
|
||||
if (bufferedWidth <= 0) return const SizedBox.shrink();
|
||||
|
||||
final leftOffset = startProgress * containerWidth;
|
||||
final bufferedWidthPx = bufferedWidth * containerWidth;
|
||||
|
||||
return Positioned(
|
||||
left: leftOffset,
|
||||
top: 22,
|
||||
child: Container(
|
||||
width: bufferedWidthPx,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
Center(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: progress,
|
||||
child: Container(
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Ползунок
|
||||
Center(
|
||||
child: Align(
|
||||
alignment: Alignment(progress * 2 - 1, 0),
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpeedBottomSheet extends StatelessWidget {
|
||||
final double currentSpeed;
|
||||
final Function(double) onSpeedSelected;
|
||||
|
||||
const _SpeedBottomSheet({
|
||||
required this.currentSpeed,
|
||||
required this.onSpeedSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final speeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dialogBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[400],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Скорость воспроизведения',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
children: speeds.map((speed) {
|
||||
final isSelected = speed == currentSpeed;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => onSpeedSelected(speed),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${speed}x',
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorWidget extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
|
||||
const _ErrorWidget({required this.colorScheme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: colorScheme.onErrorContainer,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Не удалось загрузить видео',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Проверьте подключение к интернету\nили попробуйте позже',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/widgets/chat_message_bubble.dart';
|
||||
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
import 'package:gwid/screens/chat_screen.dart';
|
||||
|
||||
class ControlMessageChip extends StatelessWidget {
|
||||
final Message message;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
|
||||
42
macos/Podfile
Normal file
42
macos/Podfile
Normal file
@@ -0,0 +1,42 @@
|
||||
platform :osx, '10.15'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
||||
@@ -27,6 +27,8 @@
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
3BE50CEC3B857AFB79ED4B51 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 388CA5C37E59612ABAEE6B9C /* Pods_RunnerTests.framework */; };
|
||||
CDF433146D1871A7EB701871 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89C4DA26E1D86992C17E0203 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -60,11 +62,13 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1513A8E85A871669A708EFD4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
1C6F713FF81D9CB5A449F94D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* gwid.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gwid.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* gwid.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = gwid.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@@ -76,8 +80,14 @@
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
388CA5C37E59612ABAEE6B9C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
49D5476057F69A4097C12C58 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
6C72571057485AEF9EBFBCC3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
8028056045BFBF1AE7C02DD1 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
89C4DA26E1D86992C17E0203 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
FE5B3021B816A2FD94CBB7E1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -85,6 +95,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3BE50CEC3B857AFB79ED4B51 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -92,6 +103,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CDF433146D1871A7EB701871 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -125,6 +137,7 @@
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
C9FFC0A80D79F5C5380DF8BD /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -172,9 +185,25 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C9FFC0A80D79F5C5380DF8BD /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1513A8E85A871669A708EFD4 /* Pods-Runner.debug.xcconfig */,
|
||||
49D5476057F69A4097C12C58 /* Pods-Runner.release.xcconfig */,
|
||||
FE5B3021B816A2FD94CBB7E1 /* Pods-Runner.profile.xcconfig */,
|
||||
6C72571057485AEF9EBFBCC3 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
1C6F713FF81D9CB5A449F94D /* Pods-RunnerTests.release.xcconfig */,
|
||||
8028056045BFBF1AE7C02DD1 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
89C4DA26E1D86992C17E0203 /* Pods_Runner.framework */,
|
||||
388CA5C37E59612ABAEE6B9C /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -186,6 +215,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
FBA29210F50B351DCEE6280B /* [CP] Check Pods Manifest.lock */,
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
@@ -204,11 +234,13 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
FBA311D2EFFD2A674FD24C1D /* [CP] Check Pods Manifest.lock */,
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
2DB171D51BA2A164FD6822FA /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -291,6 +323,23 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
2DB171D51BA2A164FD6822FA /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -329,6 +378,50 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
FBA29210F50B351DCEE6280B /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FBA311D2EFFD2A674FD24C1D /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -380,6 +473,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6C72571057485AEF9EBFBCC3 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -394,6 +488,7 @@
|
||||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1C6F713FF81D9CB5A449F94D /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -408,6 +503,7 @@
|
||||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 8028056045BFBF1AE7C02DD1 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -8,5 +8,9 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<string>Komet</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
1551
pubspec.lock
1551
pubspec.lock
File diff suppressed because it is too large
Load Diff
@@ -108,6 +108,8 @@ dependencies:
|
||||
|
||||
msgpack_dart: ^1.0.1
|
||||
|
||||
ffi: ^2.1.0
|
||||
|
||||
disable_battery_optimization: ^1.1.2
|
||||
|
||||
flutter_highlight: ^0.7.0
|
||||
@@ -123,6 +125,7 @@ dependencies:
|
||||
chewie: ^1.7.5
|
||||
|
||||
just_audio: ^0.9.40
|
||||
platform_info: ^5.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user