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/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/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"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.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_logger.dart';
|
||||||
import 'package:gwid/connection/connection_state.dart' as conn_state;
|
import 'package:gwid/connection/connection_state.dart' as conn_state;
|
||||||
import 'package:gwid/connection/health_monitor.dart';
|
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/complaint.dart';
|
||||||
import 'package:gwid/models/contact.dart';
|
import 'package:gwid/models/contact.dart';
|
||||||
import 'package:gwid/models/message.dart';
|
import 'package:gwid/models/message.dart';
|
||||||
import 'package:gwid/models/profile.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/account_manager.dart';
|
||||||
import 'package:gwid/services/avatar_cache_service.dart';
|
import 'package:gwid/services/avatar_cache_service.dart';
|
||||||
import 'package:gwid/services/cache_service.dart';
|
import 'package:gwid/services/cache_service.dart';
|
||||||
import 'package:gwid/services/chat_cache_service.dart';
|
import 'package:gwid/services/chat_cache_service.dart';
|
||||||
import 'package:gwid/services/profile_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:http/http.dart' as http;
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|||||||
@@ -255,4 +255,113 @@ extension ApiServiceAuth on ApiService {
|
|||||||
rethrow;
|
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'});
|
_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 {
|
Future<void> subscribeToChat(int chatId, bool subscribe) async {
|
||||||
await waitUntilOnline();
|
await waitUntilOnline();
|
||||||
_sendMessage(75, {'chatId': chatId, 'subscribe': subscribe});
|
_sendMessage(75, {'chatId': chatId, 'subscribe': subscribe});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'theme_provider.dart';
|
import 'utils/theme_provider.dart';
|
||||||
|
|
||||||
class ConnectionLifecycleManager extends StatefulWidget {
|
class ConnectionLifecycleManager extends StatefulWidget {
|
||||||
final Widget child;
|
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:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
import 'phone_entry_screen.dart';
|
import 'screens/phone_entry_screen.dart';
|
||||||
import 'theme_provider.dart';
|
import 'utils/theme_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/models/channel.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 {
|
class ChannelsListScreen extends StatefulWidget {
|
||||||
const ChannelsListScreen({super.key});
|
const ChannelsListScreen({super.key});
|
||||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.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/api/api_service.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:gwid/models/contact.dart';
|
import 'package:gwid/models/contact.dart';
|
||||||
@@ -6,32 +6,31 @@ import 'package:flutter/scheduler.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:gwid/chat_screen.dart';
|
import 'package:gwid/screens/chat_screen.dart';
|
||||||
import 'package:gwid/manage_account_screen.dart';
|
import 'package:gwid/screens/manage_account_screen.dart';
|
||||||
import 'package:gwid/screens/settings/settings_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/chat.dart';
|
||||||
import 'package:gwid/models/contact.dart';
|
import 'package:gwid/models/contact.dart';
|
||||||
import 'package:gwid/models/message.dart';
|
import 'package:gwid/models/message.dart';
|
||||||
import 'package:gwid/models/profile.dart';
|
import 'package:gwid/models/profile.dart';
|
||||||
import 'package:gwid/models/chat_folder.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:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:gwid/join_group_screen.dart';
|
import 'package:gwid/screens/join_group_screen.dart';
|
||||||
import 'package:gwid/search_contact_screen.dart';
|
import 'package:gwid/screens/search_contact_screen.dart';
|
||||||
import 'package:gwid/channels_list_screen.dart';
|
import 'package:gwid/screens/channels_list_screen.dart';
|
||||||
import 'package:gwid/models/channel.dart';
|
import 'package:gwid/models/channel.dart';
|
||||||
import 'package:gwid/search_channels_screen.dart';
|
import 'package:gwid/screens/search_channels_screen.dart';
|
||||||
import 'package:gwid/downloads_screen.dart';
|
import 'package:gwid/screens/downloads_screen.dart';
|
||||||
import 'package:gwid/user_id_lookup_screen.dart';
|
import 'package:gwid/utils/user_id_lookup_screen.dart';
|
||||||
import 'package:gwid/screens/music_library_screen.dart';
|
import 'package:gwid/screens/music_library_screen.dart';
|
||||||
import 'package:gwid/widgets/message_preview_dialog.dart';
|
import 'package:gwid/widgets/message_preview_dialog.dart';
|
||||||
import 'package:gwid/services/chat_read_settings_service.dart';
|
import 'package:gwid/services/chat_read_settings_service.dart';
|
||||||
import 'package:gwid/services/local_profile_manager.dart';
|
import 'package:gwid/services/local_profile_manager.dart';
|
||||||
import 'package:gwid/widgets/contact_name_widget.dart';
|
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||||
import 'package:gwid/widgets/contact_avatar_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/services/account_manager.dart';
|
||||||
import 'package:gwid/models/account.dart';
|
import 'package:gwid/models/account.dart';
|
||||||
|
|
||||||
@@ -745,7 +744,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
/*const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.search, size: 20),
|
icon: const Icon(Icons.search, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -756,7 +755,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
tooltip: 'Поиск каналов',
|
tooltip: 'Поиск каналов',
|
||||||
),
|
),*/
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -995,7 +994,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
ListTile(
|
/*ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -1015,7 +1014,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),*/
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
@@ -2032,7 +2031,24 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
size: 20,
|
size: 20,
|
||||||
)
|
)
|
||||||
: null,
|
: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 20,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_showDeleteAccountDialog(
|
||||||
|
context,
|
||||||
|
account,
|
||||||
|
accountManager,
|
||||||
|
() {
|
||||||
|
// Обновляем список аккаунтов
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
onTap: isCurrent
|
onTap: isCurrent
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () 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() {
|
void _showSearchFilters() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/scheduler.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: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/api/api_service.dart';
|
||||||
import 'package:gwid/phone_entry_screen.dart';
|
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||||
import 'package:gwid/custom_request_screen.dart';
|
import 'package:gwid/screens/custom_request_screen.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class DebugScreen extends StatelessWidget {
|
class DebugScreen extends StatelessWidget {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/chats_screen.dart';
|
import 'package:gwid/screens/chats_screen.dart';
|
||||||
import 'package:gwid/phone_entry_screen.dart';
|
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/screens/settings/reconnection_screen.dart';
|
import 'package:gwid/screens/settings/reconnection_screen.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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/chat.dart';
|
||||||
import 'package:gwid/models/contact.dart';
|
import 'package:gwid/models/contact.dart';
|
||||||
import 'package:gwid/models/profile.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:provider/provider.dart';
|
||||||
import 'package:gwid/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/models/profile.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/profile_cache_service.dart';
|
||||||
import 'package:gwid/services/local_profile_manager.dart';
|
import 'package:gwid/services/local_profile_manager.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:pinput/pinput.dart';
|
import 'package:pinput/pinput.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/chats_screen.dart';
|
import 'package:gwid/screens/chats_screen.dart';
|
||||||
import 'package:gwid/password_auth_screen.dart';
|
import 'package:gwid/screens/password_auth_screen.dart';
|
||||||
|
|
||||||
class OTPScreen extends StatefulWidget {
|
class OTPScreen extends StatefulWidget {
|
||||||
final String phoneNumber;
|
final String phoneNumber;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/api/api_service.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 {
|
class PasswordAuthScreen extends StatefulWidget {
|
||||||
const PasswordAuthScreen({super.key});
|
const PasswordAuthScreen({super.key});
|
||||||
@@ -4,14 +4,16 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/otp_screen.dart';
|
import 'package:gwid/screens/otp_screen.dart';
|
||||||
import 'package:gwid/proxy_service.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/screens/settings/auth_settings_screen.dart';
|
||||||
import 'package:gwid/token_auth_screen.dart';
|
import 'package:gwid/screens/token_auth_screen.dart';
|
||||||
import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS
|
import 'package:gwid/screens/tos_screen.dart'; // Импорт экрана ToS
|
||||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:platform_info/platform_info.dart';
|
||||||
|
|
||||||
class Country {
|
class Country {
|
||||||
final String name;
|
final String name;
|
||||||
@@ -48,6 +50,55 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
mask: '+7 (###) ###-##-##',
|
mask: '+7 (###) ###-##-##',
|
||||||
digits: 10,
|
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(
|
Country(
|
||||||
name: 'Беларусь',
|
name: 'Беларусь',
|
||||||
code: '+375',
|
code: '+375',
|
||||||
@@ -55,6 +106,13 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
mask: '+375 (##) ###-##-##',
|
mask: '+375 (##) ###-##-##',
|
||||||
digits: 9,
|
digits: 9,
|
||||||
),
|
),
|
||||||
|
Country(
|
||||||
|
name: 'Свое',
|
||||||
|
code: '',
|
||||||
|
flag: '',
|
||||||
|
mask: '',
|
||||||
|
digits: 0, // Без ограничения
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
Country _selectedCountry = _countries[0];
|
Country _selectedCountry = _countries[0];
|
||||||
@@ -66,6 +124,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
StreamSubscription? _apiSubscription;
|
StreamSubscription? _apiSubscription;
|
||||||
bool _showContent = false;
|
bool _showContent = false;
|
||||||
bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения
|
bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения
|
||||||
|
String _customPrefix = ''; // Для "Свой префикс"
|
||||||
|
|
||||||
late final AnimationController _animationController;
|
late final AnimationController _animationController;
|
||||||
late final Animation<Alignment> _topAlignmentAnimation;
|
late final Animation<Alignment> _topAlignmentAnimation;
|
||||||
@@ -120,8 +179,11 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
final payload = message['payload'];
|
final payload = message['payload'];
|
||||||
if (payload != null && payload['token'] != null) {
|
if (payload != null && payload['token'] != null) {
|
||||||
final String token = payload['token'];
|
final String token = payload['token'];
|
||||||
|
final String prefix = _selectedCountry.mask.isEmpty
|
||||||
|
? _customPrefix
|
||||||
|
: _selectedCountry.code;
|
||||||
final String fullPhoneNumber =
|
final String fullPhoneNumber =
|
||||||
_selectedCountry.code + _maskFormatter.getUnmaskedText();
|
prefix + _maskFormatter.getUnmaskedText();
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
@@ -141,14 +203,23 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initializeMaskFormatter() {
|
void _initializeMaskFormatter() {
|
||||||
final mask = _selectedCountry.mask
|
if (_selectedCountry.mask.isEmpty) {
|
||||||
.replaceFirst(RegExp(r'^\+\d+\s?'), '')
|
// Для "Свой префикс" - без маски, только цифры
|
||||||
.trim();
|
_maskFormatter = MaskTextInputFormatter(
|
||||||
_maskFormatter = MaskTextInputFormatter(
|
mask: '',
|
||||||
mask: mask,
|
filter: {"#": RegExp(r'[0-9]')},
|
||||||
filter: {"#": RegExp(r'[0-9]')},
|
type: MaskAutoCompletionType.lazy,
|
||||||
type: MaskAutoCompletionType.lazy,
|
);
|
||||||
);
|
} else {
|
||||||
|
final mask = _selectedCountry.mask
|
||||||
|
.replaceFirst(RegExp(r'^\+\d+\s?'), '')
|
||||||
|
.trim();
|
||||||
|
_maskFormatter = MaskTextInputFormatter(
|
||||||
|
mask: mask,
|
||||||
|
filter: {"#": RegExp(r'[0-9]')},
|
||||||
|
type: MaskAutoCompletionType.lazy,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPhoneChanged() {
|
void _onPhoneChanged() {
|
||||||
@@ -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) {
|
if (isFull != _isButtonEnabled) {
|
||||||
setState(() => _isButtonEnabled = isFull);
|
setState(() => _isButtonEnabled = isFull);
|
||||||
}
|
}
|
||||||
@@ -192,17 +266,78 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCountryChanged(Country? country) {
|
void _onCountryChanged(Country? country) async {
|
||||||
if (country != null && country != _selectedCountry) {
|
if (country != null && country != _selectedCountry) {
|
||||||
setState(() {
|
// Если выбран "Свой префикс", показываем диалог для ввода префикса
|
||||||
_selectedCountry = country;
|
if (country.mask.isEmpty) {
|
||||||
_phoneController.clear();
|
final prefix = await _showCustomPrefixDialog();
|
||||||
_initializeMaskFormatter();
|
if (prefix == null || prefix.isEmpty) {
|
||||||
_isButtonEnabled = false;
|
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 {
|
void _checkAnonymitySettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final anonymityEnabled = prefs.getBool('anonymity_enabled') ?? false;
|
final anonymityEnabled = prefs.getBool('anonymity_enabled') ?? false;
|
||||||
@@ -221,8 +356,10 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
void _requestOtp() async {
|
void _requestOtp() async {
|
||||||
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
|
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final String fullPhoneNumber =
|
final String prefix = _selectedCountry.mask.isEmpty
|
||||||
_selectedCountry.code + _maskFormatter.getUnmaskedText();
|
? _customPrefix
|
||||||
|
: _selectedCountry.code;
|
||||||
|
final String fullPhoneNumber = prefix + _maskFormatter.getUnmaskedText();
|
||||||
try {
|
try {
|
||||||
ApiService.instance.errorStream.listen((error) {
|
ApiService.instance.errorStream.listen((error) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -341,7 +478,44 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
selectedCountry: _selectedCountry,
|
selectedCountry: _selectedCountry,
|
||||||
countries: _countries,
|
countries: _countries,
|
||||||
onCountryChanged: _onCountryChanged,
|
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),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@@ -517,6 +691,7 @@ class _PhoneInput extends StatelessWidget {
|
|||||||
final Country selectedCountry;
|
final Country selectedCountry;
|
||||||
final List<Country> countries;
|
final List<Country> countries;
|
||||||
final ValueChanged<Country?> onCountryChanged;
|
final ValueChanged<Country?> onCountryChanged;
|
||||||
|
final String customPrefix;
|
||||||
|
|
||||||
const _PhoneInput({
|
const _PhoneInput({
|
||||||
required this.phoneController,
|
required this.phoneController,
|
||||||
@@ -524,6 +699,7 @@ class _PhoneInput extends StatelessWidget {
|
|||||||
required this.selectedCountry,
|
required this.selectedCountry,
|
||||||
required this.countries,
|
required this.countries,
|
||||||
required this.onCountryChanged,
|
required this.onCountryChanged,
|
||||||
|
required this.customPrefix,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -542,6 +718,7 @@ class _PhoneInput extends StatelessWidget {
|
|||||||
selectedCountry: selectedCountry,
|
selectedCountry: selectedCountry,
|
||||||
countries: countries,
|
countries: countries,
|
||||||
onCountryChanged: onCountryChanged,
|
onCountryChanged: onCountryChanged,
|
||||||
|
customPrefix: customPrefix,
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
@@ -556,11 +733,13 @@ class _CountryPicker extends StatelessWidget {
|
|||||||
final Country selectedCountry;
|
final Country selectedCountry;
|
||||||
final List<Country> countries;
|
final List<Country> countries;
|
||||||
final ValueChanged<Country?> onCountryChanged;
|
final ValueChanged<Country?> onCountryChanged;
|
||||||
|
final String customPrefix;
|
||||||
|
|
||||||
const _CountryPicker({
|
const _CountryPicker({
|
||||||
required this.selectedCountry,
|
required this.selectedCountry,
|
||||||
required this.countries,
|
required this.countries,
|
||||||
required this.onCountryChanged,
|
required this.onCountryChanged,
|
||||||
|
required this.customPrefix,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -575,16 +754,40 @@ class _CountryPicker extends StatelessWidget {
|
|||||||
value: selectedCountry,
|
value: selectedCountry,
|
||||||
onChanged: onCountryChanged,
|
onChanged: onCountryChanged,
|
||||||
icon: Icon(Icons.keyboard_arrow_down, color: colors.onSurfaceVariant),
|
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) {
|
items: countries.map((Country country) {
|
||||||
return DropdownMenuItem<Country>(
|
return DropdownMenuItem<Country>(
|
||||||
value: country,
|
value: country,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(country.flag, style: textTheme.titleMedium),
|
if (country.flag.isNotEmpty) ...[
|
||||||
const SizedBox(width: 8),
|
Text(country.flag, style: textTheme.titleMedium),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
Text(
|
Text(
|
||||||
country.code,
|
country.code.isEmpty ? 'Свое' : country.code,
|
||||||
style: GoogleFonts.manrope(
|
style: GoogleFonts.manrope(
|
||||||
textStyle: textTheme.titleMedium,
|
textStyle: textTheme.titleMedium,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/models/profile.dart';
|
||||||
import 'package:gwid/screens/settings/settings_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:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:gwid/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
|
|
||||||
class ProfileMenuDialog extends StatefulWidget {
|
class ProfileMenuDialog extends StatefulWidget {
|
||||||
final Profile? myProfile;
|
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 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/models/contact.dart';
|
import 'package:gwid/models/contact.dart';
|
||||||
|
import 'package:gwid/screens/chat_screen.dart';
|
||||||
|
|
||||||
class SearchContactScreen extends StatefulWidget {
|
class SearchContactScreen extends StatefulWidget {
|
||||||
const SearchContactScreen({super.key});
|
const SearchContactScreen({super.key});
|
||||||
@@ -36,7 +35,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
|||||||
_apiSubscription = ApiService.instance.messages.listen((message) {
|
_apiSubscription = ApiService.instance.messages.listen((message) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|
||||||
if (message['type'] == 'contact_found') {
|
if (message['type'] == 'contact_found') {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -48,22 +46,24 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
|||||||
|
|
||||||
if (contactData != null) {
|
if (contactData != null) {
|
||||||
_foundContact = Contact.fromJson(contactData);
|
_foundContact = Contact.fromJson(contactData);
|
||||||
}
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
// Автоматически открываем чат с найденным контактом
|
||||||
SnackBar(
|
_openChatWithContact(_foundContact!);
|
||||||
content: const Text('Контакт найден!'),
|
} else {
|
||||||
backgroundColor: Colors.green,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
shape: RoundedRectangleBorder(
|
SnackBar(
|
||||||
borderRadius: BorderRadius.circular(12),
|
content: const Text('Контакт найден!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: const EdgeInsets.all(10),
|
||||||
),
|
),
|
||||||
behavior: SnackBarBehavior.floating,
|
);
|
||||||
margin: const EdgeInsets.all(10),
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (message['type'] == 'contact_not_found') {
|
if (message['type'] == 'contact_not_found') {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -118,7 +118,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!phone.startsWith('+') || phone.length < 10) {
|
if (!phone.startsWith('+') || phone.length < 10) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -160,20 +159,146 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: const Text('Не удалось найти чат с этим контактом'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
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() {
|
void _startChat() {
|
||||||
if (_foundContact != null) {
|
if (_foundContact != null) {
|
||||||
|
_openChatWithContact(_foundContact!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
Future<void> _startChatAlternative() async {
|
||||||
SnackBar(
|
if (_foundContact == null) return;
|
||||||
content: Text('Создание чата с ${_foundContact!.name}'),
|
|
||||||
backgroundColor: Colors.blue,
|
try {
|
||||||
shape: RoundedRectangleBorder(
|
setState(() {
|
||||||
borderRadius: BorderRadius.circular(12),
|
_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('Понятно'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
behavior: SnackBarBehavior.floating,
|
);
|
||||||
margin: const EdgeInsets.all(10),
|
}
|
||||||
),
|
} 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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -230,7 +354,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
|||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'Номер телефона',
|
'Номер телефона',
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
@@ -322,7 +445,6 @@ class _SearchContactScreenState extends State<SearchContactScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
if (_foundContact != null) ...[
|
if (_foundContact != null) ...[
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Container(
|
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) ...[
|
if (_errorMessage != null) ...[
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Container(
|
Container(
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class AboutScreen extends StatelessWidget {
|
class AboutScreen extends StatelessWidget {
|
||||||
@@ -239,7 +240,7 @@ class AboutScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Версия 0.3.0',
|
'Версия $version',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: colors.onSurface.withOpacity(0.7),
|
color: colors.onSurface.withOpacity(0.7),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:gwid/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
|
|
||||||
|
|
||||||
class AnimationsScreen extends StatelessWidget {
|
class AnimationsScreen extends StatelessWidget {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.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/customization_screen.dart';
|
||||||
import 'package:gwid/screens/settings/animations_screen.dart';
|
import 'package:gwid/screens/settings/animations_screen.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.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/proxy_settings_screen.dart';
|
||||||
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:gwid/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
|
|
||||||
class BypassScreen extends StatelessWidget {
|
class BypassScreen extends StatelessWidget {
|
||||||
final bool isModal;
|
final bool isModal;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:gwid/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:gwid/models/message.dart';
|
import 'package:gwid/models/message.dart';
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/proxy_service.dart';
|
import 'package:gwid/utils/proxy_service.dart';
|
||||||
import 'package:gwid/spoofing_service.dart';
|
import 'package:gwid/utils/spoofing_service.dart';
|
||||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/api/api_service.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:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.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 {
|
class PrivacySettingsScreen extends StatefulWidget {
|
||||||
const PrivacySettingsScreen({super.key});
|
const PrivacySettingsScreen({super.key});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/proxy_service.dart';
|
import 'package:gwid/utils/proxy_service.dart';
|
||||||
import 'package:gwid/proxy_settings.dart';
|
import 'package:gwid/utils/proxy_settings.dart';
|
||||||
|
|
||||||
class ProxySettingsScreen extends StatefulWidget {
|
class ProxySettingsScreen extends StatefulWidget {
|
||||||
const ProxySettingsScreen({super.key});
|
const ProxySettingsScreen({super.key});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/api/api_service.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 {
|
class ReconnectionScreen extends StatefulWidget {
|
||||||
const ReconnectionScreen({super.key});
|
const ReconnectionScreen({super.key});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:flutter_timezone/flutter_timezone.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:gwid/device_presets.dart';
|
import 'package:gwid/utils/device_presets.dart';
|
||||||
|
|
||||||
enum SpoofingMethod { partial, full }
|
enum SpoofingMethod { partial, full }
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gwid/consts.dart';
|
||||||
import 'package:gwid/models/profile.dart';
|
import 'package:gwid/models/profile.dart';
|
||||||
import 'package:gwid/api/api_service.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/appearance_settings_screen.dart';
|
||||||
import 'package:gwid/screens/settings/notification_settings_screen.dart';
|
import 'package:gwid/screens/settings/notification_settings_screen.dart';
|
||||||
import 'package:gwid/screens/settings/privacy_security_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/network_settings_screen.dart';
|
||||||
import 'package:gwid/screens/settings/bypass_screen.dart';
|
import 'package:gwid/screens/settings/bypass_screen.dart';
|
||||||
import 'package:gwid/screens/settings/about_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/screens/settings/komet_misc_screen.dart';
|
||||||
import 'package:gwid/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
@@ -208,7 +209,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
body: _buildSettingsContent(),
|
body: SafeArea(
|
||||||
|
child: _buildSettingsContent(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,7 +471,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'v0.3.0-beta.1',
|
version,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
|
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/home_screen.dart';
|
import 'package:gwid/screens/home_screen.dart';
|
||||||
import 'package:gwid/proxy_service.dart';
|
import 'package:gwid/utils/proxy_service.dart';
|
||||||
import 'package:gwid/proxy_settings.dart';
|
import 'package:gwid/utils/proxy_settings.dart';
|
||||||
import 'package:gwid/screens/settings/qr_scanner_screen.dart';
|
import 'package:gwid/screens/settings/qr_scanner_screen.dart';
|
||||||
import 'package:gwid/screens/settings/session_spoofing_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:intl/intl.dart';
|
||||||
import 'package:gwid/models/message.dart';
|
import 'package:gwid/models/message.dart';
|
||||||
import 'package:gwid/models/contact.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:provider/provider.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:url_launcher/url_launcher.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/services/avatar_cache_service.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:open_file/open_file.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:just_audio/just_audio.dart';
|
||||||
import 'package:gwid/services/cache_service.dart';
|
import 'package:gwid/services/cache_service.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import 'package:gwid/services/music_player_service.dart';
|
import 'package:gwid/services/music_player_service.dart';
|
||||||
|
import 'package:platform_info/platform_info.dart';
|
||||||
|
|
||||||
bool _currentIsDark = false;
|
bool _currentIsDark = false;
|
||||||
|
|
||||||
|
bool isMobile = Platform.instance.operatingSystem.iOS ||
|
||||||
|
Platform.instance.operatingSystem.android;
|
||||||
|
|
||||||
enum MessageReadStatus {
|
enum MessageReadStatus {
|
||||||
sending, // Отправляется (часы)
|
sending, // Отправляется (часы)
|
||||||
sent, // Отправлено (1 галочка)
|
sent, // Отправлено (1 галочка)
|
||||||
@@ -1081,12 +1085,22 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||||
messageContent = GestureDetector(
|
if (isMobile) {
|
||||||
|
messageContent = GestureDetector(
|
||||||
onTapDown: (TapDownDetails details) {
|
onTapDown: (TapDownDetails details) {
|
||||||
_showMessageContextMenu(context, details.globalPosition);
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
},
|
},
|
||||||
child: messageContent,
|
child: messageContent,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
messageContent = GestureDetector(
|
||||||
|
onSecondaryTapDown: (TapDownDetails details) {
|
||||||
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
|
},
|
||||||
|
child: messageContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -1535,12 +1549,21 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||||
videoContent = GestureDetector(
|
if (isMobile) {
|
||||||
|
videoContent = GestureDetector(
|
||||||
onTapDown: (TapDownDetails details) {
|
onTapDown: (TapDownDetails details) {
|
||||||
_showMessageContextMenu(context, details.globalPosition);
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
},
|
},
|
||||||
child: videoContent,
|
child: videoContent,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
videoContent = GestureDetector(
|
||||||
|
onSecondaryTapDown: (TapDownDetails details) {
|
||||||
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
|
},
|
||||||
|
child: videoContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoContent;
|
return videoContent;
|
||||||
@@ -1621,12 +1644,21 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||||
photoContent = GestureDetector(
|
if (isMobile) {
|
||||||
|
photoContent = GestureDetector(
|
||||||
onTapDown: (TapDownDetails details) {
|
onTapDown: (TapDownDetails details) {
|
||||||
_showMessageContextMenu(context, details.globalPosition);
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
},
|
},
|
||||||
child: photoContent,
|
child: photoContent,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
photoContent = GestureDetector(
|
||||||
|
onTapDown: (TapDownDetails details) {
|
||||||
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
|
},
|
||||||
|
child: photoContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return photoContent;
|
return photoContent;
|
||||||
@@ -1749,12 +1781,21 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) {
|
||||||
videoContent = GestureDetector(
|
if (isMobile) {
|
||||||
|
videoContent = GestureDetector(
|
||||||
onTapDown: (TapDownDetails details) {
|
onTapDown: (TapDownDetails details) {
|
||||||
_showMessageContextMenu(context, details.globalPosition);
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
},
|
},
|
||||||
child: videoContent,
|
child: videoContent,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
videoContent = GestureDetector(
|
||||||
|
onSecondaryTapDown: (TapDownDetails details) {
|
||||||
|
_showMessageContextMenu(context, details.globalPosition);
|
||||||
|
},
|
||||||
|
child: videoContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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/api/api_service.dart';
|
||||||
import 'package:gwid/widgets/chat_message_bubble.dart';
|
import 'package:gwid/widgets/chat_message_bubble.dart';
|
||||||
import 'package:gwid/widgets/contact_name_widget.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 {
|
class ControlMessageChip extends StatelessWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.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"
|
#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 */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -60,11 +62,13 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -85,6 +95,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
3BE50CEC3B857AFB79ED4B51 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -92,6 +103,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
CDF433146D1871A7EB701871 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -125,6 +137,7 @@
|
|||||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
33CC10EE2044A3C60003C045 /* Products */,
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||||
|
C9FFC0A80D79F5C5380DF8BD /* Pods */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -172,9 +185,25 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
89C4DA26E1D86992C17E0203 /* Pods_Runner.framework */,
|
||||||
|
388CA5C37E59612ABAEE6B9C /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -186,6 +215,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
FBA29210F50B351DCEE6280B /* [CP] Check Pods Manifest.lock */,
|
||||||
331C80D1294CF70F00263BE5 /* Sources */,
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
331C80D3294CF70F00263BE5 /* Resources */,
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
@@ -204,11 +234,13 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
FBA311D2EFFD2A674FD24C1D /* [CP] Check Pods Manifest.lock */,
|
||||||
33CC10E92044A3C60003C045 /* Sources */,
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
|
2DB171D51BA2A164FD6822FA /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -291,6 +323,23 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase 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 */ = {
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -329,6 +378,50 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -380,6 +473,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 6C72571057485AEF9EBFBCC3 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -394,6 +488,7 @@
|
|||||||
};
|
};
|
||||||
331C80DC294CF71000263BE5 /* Release */ = {
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1C6F713FF81D9CB5A449F94D /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -408,6 +503,7 @@
|
|||||||
};
|
};
|
||||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 8028056045BFBF1AE7C02DD1 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|||||||
@@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>Komet</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@@ -4,5 +4,9 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
1551
pubspec.lock
1551
pubspec.lock
File diff suppressed because it is too large
Load Diff
@@ -106,7 +106,9 @@ dependencies:
|
|||||||
|
|
||||||
es_compression: ^2.0.14
|
es_compression: ^2.0.14
|
||||||
|
|
||||||
msgpack_dart: ^1.0.1
|
msgpack_dart: ^1.0.1
|
||||||
|
|
||||||
|
ffi: ^2.1.0
|
||||||
|
|
||||||
disable_battery_optimization: ^1.1.2
|
disable_battery_optimization: ^1.1.2
|
||||||
|
|
||||||
@@ -123,6 +125,7 @@ dependencies:
|
|||||||
chewie: ^1.7.5
|
chewie: ^1.7.5
|
||||||
|
|
||||||
just_audio: ^0.9.40
|
just_audio: ^0.9.40
|
||||||
|
platform_info: ^5.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user