diff --git a/.gitignore b/.gitignore index 3820a95..8f7bee0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +pubspec.lock +.vscode/ +macos/Podfile.lock diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 337b1e3..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cmake.sourceDirectory": "C:/Users/Kiril/OneDrive/Desktop/KOMET/KOMETABLYATSKAYA/app/linux" -} \ No newline at end of file diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -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 diff --git a/lib/api/api_registration_service.dart b/lib/api/api_registration_service.dart new file mode 100644 index 0000000..3f33072 --- /dev/null +++ b/lib/api/api_registration_service.dart @@ -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 src, + Pointer dst, + Int32 compressedSize, + Int32 dstCapacity, + ); +typedef Lz4Decompress = + int Function( + Pointer src, + Pointer dst, + int compressedSize, + int dstCapacity, + ); + +class RegistrationService { + Socket? _socket; + int _seq = 0; + final Map> _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>( + 'LZ4_decompress_safe', + ) + .asFunction(); + print('✅ LZ4 block decompress функция загружена'); + } catch (e) { + print( + '⚠️ Функция LZ4_decompress_safe не найдена, пробуем альтернативные имена...', + ); + // Пробуем другие возможные имена + try { + _lz4BlockDecompress = _lz4Lib! + .lookup>( + '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 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 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(srcSize); + final dstPtr = malloc.allocate(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(srcSize); + final dstPtr = malloc.allocate(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 _sendMessage(int opcode, Map 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(); + _pending[seq] = completer; + + _socket!.add(packet); + await _socket!.flush(); + + return completer.future.timeout(const Duration(seconds: 30)); + } + + Future 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 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 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'); + } + } +} diff --git a/lib/api/api_service.dart b/lib/api/api_service.dart index 7b01e8d..f8f3103 100644 --- a/lib/api/api_service.dart +++ b/lib/api/api_service.dart @@ -8,18 +8,18 @@ import 'package:flutter/services.dart'; import 'package:gwid/connection/connection_logger.dart'; import 'package:gwid/connection/connection_state.dart' as conn_state; import 'package:gwid/connection/health_monitor.dart'; -import 'package:gwid/image_cache_service.dart'; +import 'package:gwid/utils/image_cache_service.dart'; import 'package:gwid/models/complaint.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/models/profile.dart'; -import 'package:gwid/proxy_service.dart'; +import 'package:gwid/utils/proxy_service.dart'; import 'package:gwid/services/account_manager.dart'; import 'package:gwid/services/avatar_cache_service.dart'; import 'package:gwid/services/cache_service.dart'; import 'package:gwid/services/chat_cache_service.dart'; import 'package:gwid/services/profile_cache_service.dart'; -import 'package:gwid/spoofing_service.dart'; +import 'package:gwid/utils/spoofing_service.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/api/api_service_auth.dart b/lib/api/api_service_auth.dart index ccef8f7..39f3323 100644 --- a/lib/api/api_service_auth.dart +++ b/lib/api/api_service_auth.dart @@ -255,4 +255,113 @@ extension ApiServiceAuth on ApiService { rethrow; } } + + // Registration methods + Future 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>(); + 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 verifyRegistrationCode(String token, String code) async { + final payload = { + 'token': token, + 'verifyCode': code, + 'authTokenType': 'CHECK_CODE', + }; + + final completer = Completer>(); + 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 completeRegistration(String regToken) async { + final payload = { + "lastName": "User", + "token": regToken, + "firstName": "Komet", + "tokenType": "REGISTER", + }; + + final completer = Completer>(); + 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; + } + } } diff --git a/lib/api/api_service_contacts.dart b/lib/api/api_service_contacts.dart index 9c58fb0..e27075f 100644 --- a/lib/api/api_service_contacts.dart +++ b/lib/api/api_service_contacts.dart @@ -16,6 +16,12 @@ extension ApiServiceContacts on ApiService { _sendMessage(34, {'contactId': contactId, 'action': 'ADD'}); } + Future requestContactsByIds(List contactIds) async { + await waitUntilOnline(); + _sendMessage(35, {'contactIds': contactIds}); + print('Отправлен запрос opcode=35 с contactIds: $contactIds'); + } + Future subscribeToChat(int chatId, bool subscribe) async { await waitUntilOnline(); _sendMessage(75, {'chatId': chatId, 'subscribe': subscribe}); diff --git a/lib/connection_lifecycle_manager.dart b/lib/connection_lifecycle_manager.dart index fdffb8e..fde681b 100644 --- a/lib/connection_lifecycle_manager.dart +++ b/lib/connection_lifecycle_manager.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:gwid/api/api_service.dart'; -import 'theme_provider.dart'; +import 'utils/theme_provider.dart'; class ConnectionLifecycleManager extends StatefulWidget { final Widget child; diff --git a/lib/consts.dart b/lib/consts.dart new file mode 100644 index 0000000..7d0937f --- /dev/null +++ b/lib/consts.dart @@ -0,0 +1,2 @@ +// Датафайл с константами, полезно при изменении версии например +const version = "0.3.0"; \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7e0bb9f..27a796f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,9 +3,9 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'home_screen.dart'; -import 'phone_entry_screen.dart'; -import 'theme_provider.dart'; +import 'screens/home_screen.dart'; +import 'screens/phone_entry_screen.dart'; +import 'utils/theme_provider.dart'; import 'package:provider/provider.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; diff --git a/lib/cache_management_screen.dart b/lib/screens/cache_management_screen.dart similarity index 100% rename from lib/cache_management_screen.dart rename to lib/screens/cache_management_screen.dart diff --git a/lib/channels_list_screen.dart b/lib/screens/channels_list_screen.dart similarity index 99% rename from lib/channels_list_screen.dart rename to lib/screens/channels_list_screen.dart index a014923..167ee3e 100644 --- a/lib/channels_list_screen.dart +++ b/lib/screens/channels_list_screen.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/models/channel.dart'; -import 'package:gwid/search_channels_screen.dart'; +import 'package:gwid/screens/search_channels_screen.dart'; class ChannelsListScreen extends StatefulWidget { const ChannelsListScreen({super.key}); diff --git a/lib/chat_screen.dart b/lib/screens/chat_screen.dart similarity index 99% rename from lib/chat_screen.dart rename to lib/screens/chat_screen.dart index 518488c..8d0236b 100644 --- a/lib/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:gwid/api/api_service.dart'; import 'package:flutter/services.dart'; import 'package:gwid/models/contact.dart'; diff --git a/lib/chats_screen.dart b/lib/screens/chats_screen.dart similarity index 98% rename from lib/chats_screen.dart rename to lib/screens/chats_screen.dart index 55a40c3..e0b18fd 100644 --- a/lib/chats_screen.dart +++ b/lib/screens/chats_screen.dart @@ -6,32 +6,31 @@ import 'package:flutter/scheduler.dart'; import 'package:intl/intl.dart'; import 'package:gwid/api/api_service.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:gwid/chat_screen.dart'; -import 'package:gwid/manage_account_screen.dart'; +import 'package:gwid/screens/chat_screen.dart'; +import 'package:gwid/screens/manage_account_screen.dart'; import 'package:gwid/screens/settings/settings_screen.dart'; -import 'package:gwid/phone_entry_screen.dart'; +import 'package:gwid/screens/phone_entry_screen.dart'; import 'package:gwid/models/chat.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/models/profile.dart'; import 'package:gwid/models/chat_folder.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:gwid/join_group_screen.dart'; -import 'package:gwid/search_contact_screen.dart'; -import 'package:gwid/channels_list_screen.dart'; +import 'package:gwid/screens/join_group_screen.dart'; +import 'package:gwid/screens/search_contact_screen.dart'; +import 'package:gwid/screens/channels_list_screen.dart'; import 'package:gwid/models/channel.dart'; -import 'package:gwid/search_channels_screen.dart'; -import 'package:gwid/downloads_screen.dart'; -import 'package:gwid/user_id_lookup_screen.dart'; +import 'package:gwid/screens/search_channels_screen.dart'; +import 'package:gwid/screens/downloads_screen.dart'; +import 'package:gwid/utils/user_id_lookup_screen.dart'; import 'package:gwid/screens/music_library_screen.dart'; import 'package:gwid/widgets/message_preview_dialog.dart'; import 'package:gwid/services/chat_read_settings_service.dart'; import 'package:gwid/services/local_profile_manager.dart'; import 'package:gwid/widgets/contact_name_widget.dart'; import 'package:gwid/widgets/contact_avatar_widget.dart'; -import 'package:gwid/services/contact_local_names_service.dart'; import 'package:gwid/services/account_manager.dart'; import 'package:gwid/models/account.dart'; @@ -745,7 +744,7 @@ class _ChatsScreenState extends State fontSize: 16, ), ), - const Spacer(), + /*const Spacer(), IconButton( icon: const Icon(Icons.search, size: 20), onPressed: () { @@ -756,7 +755,7 @@ class _ChatsScreenState extends State ); }, tooltip: 'Поиск каналов', - ), + ),*/ ], ), ), @@ -995,7 +994,7 @@ class _ChatsScreenState extends State }, ), - ListTile( + /*ListTile( leading: CircleAvatar( backgroundColor: Theme.of( context, @@ -1015,7 +1014,7 @@ class _ChatsScreenState extends State ), ); }, - ), + ),*/ ListTile( leading: CircleAvatar( @@ -2032,7 +2031,24 @@ class _ChatsScreenState extends State color: colors.primary, size: 20, ) - : null, + : IconButton( + icon: Icon( + Icons.close, + size: 20, + color: colors.onSurfaceVariant, + ), + onPressed: () { + _showDeleteAccountDialog( + context, + account, + accountManager, + () { + // Обновляем список аккаунтов + setState(() {}); + }, + ); + }, + ), onTap: isCurrent ? null : () async { @@ -3711,6 +3727,57 @@ class _ChatsScreenState extends State } } + void _showDeleteAccountDialog( + BuildContext context, + Account account, + AccountManager accountManager, + VoidCallback onDeleted, + ) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Удаление аккаунта'), + content: const Text('Точно хочешь удалить аккаунт?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Нет'), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + try { + await accountManager.removeAccount(account.id); + if (mounted) { + onDeleted(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Аккаунт удален'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ошибка: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Да'), + ), + ], + ); + }, + ); + } + void _showSearchFilters() { showModalBottomSheet( context: context, diff --git a/lib/custom_request_screen.dart b/lib/screens/custom_request_screen.dart similarity index 100% rename from lib/custom_request_screen.dart rename to lib/screens/custom_request_screen.dart diff --git a/lib/debug_screen.dart b/lib/screens/debug_screen.dart similarity index 99% rename from lib/debug_screen.dart rename to lib/screens/debug_screen.dart index c80fefa..dbfe8a1 100644 --- a/lib/debug_screen.dart +++ b/lib/screens/debug_screen.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/scheduler.dart'; -import 'package:gwid/cache_management_screen.dart'; // Добавлен импорт +import 'package:gwid/screens/cache_management_screen.dart'; // Добавлен импорт import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/phone_entry_screen.dart'; -import 'package:gwid/custom_request_screen.dart'; +import 'package:gwid/screens/phone_entry_screen.dart'; +import 'package:gwid/screens/custom_request_screen.dart'; import 'dart:async'; class DebugScreen extends StatelessWidget { diff --git a/lib/downloads_screen.dart b/lib/screens/downloads_screen.dart similarity index 100% rename from lib/downloads_screen.dart rename to lib/screens/downloads_screen.dart diff --git a/lib/home_screen.dart b/lib/screens/home_screen.dart similarity index 99% rename from lib/home_screen.dart rename to lib/screens/home_screen.dart index 2fd13da..be3d3c4 100644 --- a/lib/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:gwid/chats_screen.dart'; -import 'package:gwid/phone_entry_screen.dart'; +import 'package:gwid/screens/chats_screen.dart'; +import 'package:gwid/screens/phone_entry_screen.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/screens/settings/reconnection_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -10,9 +10,9 @@ import 'package:app_links/app_links.dart'; import 'package:gwid/models/chat.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/profile.dart'; -import 'package:gwid/chat_screen.dart'; +import 'package:gwid/screens/chat_screen.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); diff --git a/lib/join_group_screen.dart b/lib/screens/join_group_screen.dart similarity index 100% rename from lib/join_group_screen.dart rename to lib/screens/join_group_screen.dart diff --git a/lib/manage_account_screen.dart b/lib/screens/manage_account_screen.dart similarity index 99% rename from lib/manage_account_screen.dart rename to lib/screens/manage_account_screen.dart index 9918ff6..911997f 100644 --- a/lib/manage_account_screen.dart +++ b/lib/screens/manage_account_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/models/profile.dart'; -import 'package:gwid/phone_entry_screen.dart'; +import 'package:gwid/screens/phone_entry_screen.dart'; import 'package:gwid/services/profile_cache_service.dart'; import 'package:gwid/services/local_profile_manager.dart'; import 'package:image_picker/image_picker.dart'; diff --git a/lib/otp_screen.dart b/lib/screens/otp_screen.dart similarity index 98% rename from lib/otp_screen.dart rename to lib/screens/otp_screen.dart index f88a18f..2871b28 100644 --- a/lib/otp_screen.dart +++ b/lib/screens/otp_screen.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:pinput/pinput.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/chats_screen.dart'; -import 'package:gwid/password_auth_screen.dart'; +import 'package:gwid/screens/chats_screen.dart'; +import 'package:gwid/screens/password_auth_screen.dart'; class OTPScreen extends StatefulWidget { final String phoneNumber; diff --git a/lib/password_auth_screen.dart b/lib/screens/password_auth_screen.dart similarity index 99% rename from lib/password_auth_screen.dart rename to lib/screens/password_auth_screen.dart index 448daa0..c469a64 100644 --- a/lib/password_auth_screen.dart +++ b/lib/screens/password_auth_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/chats_screen.dart'; +import 'package:gwid/screens/chats_screen.dart'; class PasswordAuthScreen extends StatefulWidget { const PasswordAuthScreen({super.key}); diff --git a/lib/password_management_screen.dart b/lib/screens/password_management_screen.dart similarity index 100% rename from lib/password_management_screen.dart rename to lib/screens/password_management_screen.dart diff --git a/lib/phone_entry_screen.dart b/lib/screens/phone_entry_screen.dart similarity index 76% rename from lib/phone_entry_screen.dart rename to lib/screens/phone_entry_screen.dart index 755d78d..9100dcd 100644 --- a/lib/phone_entry_screen.dart +++ b/lib/screens/phone_entry_screen.dart @@ -4,14 +4,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/otp_screen.dart'; -import 'package:gwid/proxy_service.dart'; +import 'package:gwid/screens/otp_screen.dart'; +import 'package:gwid/utils/proxy_service.dart'; +import 'package:gwid/screens/registration_screen.dart'; import 'package:gwid/screens/settings/auth_settings_screen.dart'; -import 'package:gwid/token_auth_screen.dart'; -import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS +import 'package:gwid/screens/token_auth_screen.dart'; +import 'package:gwid/screens/tos_screen.dart'; // Импорт экрана ToS import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:platform_info/platform_info.dart'; class Country { final String name; @@ -48,6 +50,55 @@ class _PhoneEntryScreenState extends State mask: '+7 (###) ###-##-##', digits: 10, ), + Country( + name: 'Азербайджан', + code: '+994', + flag: '🇦🇿', + mask: '+994 (##) ###-##-##', + digits: 9, + ), + Country( + name: 'Армения', + code: '+374', + flag: '🇦🇲', + mask: '+374 (##) ###-###', + digits: 8, + ), + Country( + name: 'Казахстан', + code: '+7', + flag: '🇰🇿', + mask: '+7 (###) ###-##-##', + digits: 10, + ), + Country( + name: 'Кыргызстан', + code: '+996', + flag: '🇰🇬', + mask: '+996 (###) ###-###', + digits: 9, + ), + Country( + name: 'Молдова', + code: '+373', + flag: '🇲🇩', + mask: '+373 (####) ####', + digits: 8, + ), + Country( + name: 'Таджикистан', + code: '+992', + flag: '🇹🇯', + mask: '+992 (##) ###-##-##', + digits: 9, + ), + Country( + name: 'Узбекистан', + code: '+998', + flag: '🇺🇿', + mask: '+998 (##) ###-##-##', + digits: 9, + ), Country( name: 'Беларусь', code: '+375', @@ -55,6 +106,13 @@ class _PhoneEntryScreenState extends State mask: '+375 (##) ###-##-##', digits: 9, ), + Country( + name: 'Свое', + code: '', + flag: '', + mask: '', + digits: 0, // Без ограничения + ), ]; Country _selectedCountry = _countries[0]; @@ -66,6 +124,7 @@ class _PhoneEntryScreenState extends State StreamSubscription? _apiSubscription; bool _showContent = false; bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения + String _customPrefix = ''; // Для "Свой префикс" late final AnimationController _animationController; late final Animation _topAlignmentAnimation; @@ -120,8 +179,11 @@ class _PhoneEntryScreenState extends State final payload = message['payload']; if (payload != null && payload['token'] != null) { final String token = payload['token']; + final String prefix = _selectedCountry.mask.isEmpty + ? _customPrefix + : _selectedCountry.code; final String fullPhoneNumber = - _selectedCountry.code + _maskFormatter.getUnmaskedText(); + prefix + _maskFormatter.getUnmaskedText(); Navigator.of(context).push( MaterialPageRoute( builder: (context) => @@ -141,14 +203,23 @@ class _PhoneEntryScreenState extends State } void _initializeMaskFormatter() { - final mask = _selectedCountry.mask - .replaceFirst(RegExp(r'^\+\d+\s?'), '') - .trim(); - _maskFormatter = MaskTextInputFormatter( - mask: mask, - filter: {"#": RegExp(r'[0-9]')}, - type: MaskAutoCompletionType.lazy, - ); + if (_selectedCountry.mask.isEmpty) { + // Для "Свой префикс" - без маски, только цифры + _maskFormatter = MaskTextInputFormatter( + mask: '', + filter: {"#": RegExp(r'[0-9]')}, + type: MaskAutoCompletionType.lazy, + ); + } else { + final mask = _selectedCountry.mask + .replaceFirst(RegExp(r'^\+\d+\s?'), '') + .trim(); + _maskFormatter = MaskTextInputFormatter( + mask: mask, + filter: {"#": RegExp(r'[0-9]')}, + type: MaskAutoCompletionType.lazy, + ); + } } void _onPhoneChanged() { @@ -165,8 +236,11 @@ class _PhoneEntryScreenState extends State }); } } - final isFull = - _maskFormatter.getUnmaskedText().length == _selectedCountry.digits; + + // Для "Свой префикс" проверяем минимальную длину (например, 5 цифр) + final isFull = _selectedCountry.mask.isEmpty + ? _maskFormatter.getUnmaskedText().length >= 5 + : _maskFormatter.getUnmaskedText().length == _selectedCountry.digits; if (isFull != _isButtonEnabled) { setState(() => _isButtonEnabled = isFull); } @@ -192,17 +266,78 @@ class _PhoneEntryScreenState extends State return null; } - void _onCountryChanged(Country? country) { + void _onCountryChanged(Country? country) async { if (country != null && country != _selectedCountry) { - setState(() { - _selectedCountry = country; - _phoneController.clear(); - _initializeMaskFormatter(); - _isButtonEnabled = false; - }); + // Если выбран "Свой префикс", показываем диалог для ввода префикса + if (country.mask.isEmpty) { + final prefix = await _showCustomPrefixDialog(); + if (prefix == null || prefix.isEmpty) { + return; // Отменено + } + setState(() { + _selectedCountry = country; + _customPrefix = prefix.startsWith('+') ? prefix : '+$prefix'; + _phoneController.clear(); + _initializeMaskFormatter(); + _isButtonEnabled = false; + }); + } else { + setState(() { + _selectedCountry = country; + _customPrefix = ''; + _phoneController.clear(); + _initializeMaskFormatter(); + _isButtonEnabled = false; + }); + } } } + Future _showCustomPrefixDialog() async { + final controller = TextEditingController(); + return showDialog( + context: context, + builder: (BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return AlertDialog( + title: Text( + 'Введите код страны', + style: GoogleFonts.manrope( + textStyle: textTheme.titleLarge, + fontWeight: FontWeight.bold, + ), + ), + content: TextField( + controller: controller, + keyboardType: TextInputType.phone, + autofocus: true, + decoration: InputDecoration( + hintText: '+123', + prefixText: '+', + border: const OutlineInputBorder(), + ), + style: GoogleFonts.manrope(textStyle: textTheme.titleMedium), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Отмена', style: GoogleFonts.manrope()), + ), + FilledButton( + onPressed: () { + final prefix = controller.text.trim(); + if (prefix.isNotEmpty) { + Navigator.of(context).pop(prefix); + } + }, + child: Text('OK', style: GoogleFonts.manrope()), + ), + ], + ); + }, + ); + } + void _checkAnonymitySettings() async { final prefs = await SharedPreferences.getInstance(); final anonymityEnabled = prefs.getBool('anonymity_enabled') ?? false; @@ -221,8 +356,10 @@ class _PhoneEntryScreenState extends State void _requestOtp() async { if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return; setState(() => _isLoading = true); - final String fullPhoneNumber = - _selectedCountry.code + _maskFormatter.getUnmaskedText(); + final String prefix = _selectedCountry.mask.isEmpty + ? _customPrefix + : _selectedCountry.code; + final String fullPhoneNumber = prefix + _maskFormatter.getUnmaskedText(); try { ApiService.instance.errorStream.listen((error) { if (mounted) { @@ -341,7 +478,44 @@ class _PhoneEntryScreenState extends State selectedCountry: _selectedCountry, countries: _countries, onCountryChanged: _onCountryChanged, + customPrefix: _customPrefix, ), + + (Platform.instance.android || Platform.instance.windows) ? Column( + children: [ + const SizedBox(height: 16), + Center( + child: TextButton( + onPressed: _isTosAccepted + ? () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + const RegistrationScreen(), + ), + ); + } + : null, + child: Text( + 'зарегистрироваться', + style: GoogleFonts.manrope( + color: _isTosAccepted + ? colors.primary + : colors.onSurfaceVariant.withOpacity( + 0.5, + ), + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + decorationColor: _isTosAccepted + ? colors.primary + : colors.onSurfaceVariant.withOpacity( + 0.5, + ), + ), + ), + ), + )] + ) : const SizedBox(), const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -517,6 +691,7 @@ class _PhoneInput extends StatelessWidget { final Country selectedCountry; final List countries; final ValueChanged onCountryChanged; + final String customPrefix; const _PhoneInput({ required this.phoneController, @@ -524,6 +699,7 @@ class _PhoneInput extends StatelessWidget { required this.selectedCountry, required this.countries, required this.onCountryChanged, + required this.customPrefix, }); @override @@ -542,6 +718,7 @@ class _PhoneInput extends StatelessWidget { selectedCountry: selectedCountry, countries: countries, onCountryChanged: onCountryChanged, + customPrefix: customPrefix, ), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), @@ -556,11 +733,13 @@ class _CountryPicker extends StatelessWidget { final Country selectedCountry; final List countries; final ValueChanged onCountryChanged; + final String customPrefix; const _CountryPicker({ required this.selectedCountry, required this.countries, required this.onCountryChanged, + required this.customPrefix, }); @override @@ -575,16 +754,40 @@ class _CountryPicker extends StatelessWidget { value: selectedCountry, onChanged: onCountryChanged, icon: Icon(Icons.keyboard_arrow_down, color: colors.onSurfaceVariant), + selectedItemBuilder: (BuildContext context) { + return countries.map((Country country) { + final displayText = country.mask.isEmpty + ? (customPrefix.isNotEmpty ? customPrefix : country.name) + : country.code; + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + displayText, + style: GoogleFonts.manrope( + textStyle: textTheme.titleMedium, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + }).toList(); + }, items: countries.map((Country country) { return DropdownMenuItem( value: country, child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(country.flag, style: textTheme.titleMedium), - const SizedBox(width: 8), + if (country.flag.isNotEmpty) ...[ + Text(country.flag, style: textTheme.titleMedium), + const SizedBox(width: 8), + ], Text( - country.code, + country.code.isEmpty ? 'Свое' : country.code, style: GoogleFonts.manrope( textStyle: textTheme.titleMedium, fontWeight: FontWeight.w600, diff --git a/lib/profile_menu_dialog.dart b/lib/screens/profile_menu_dialog.dart similarity index 98% rename from lib/profile_menu_dialog.dart rename to lib/screens/profile_menu_dialog.dart index f9828e1..a71031c 100644 --- a/lib/profile_menu_dialog.dart +++ b/lib/screens/profile_menu_dialog.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:gwid/manage_account_screen.dart'; +import 'package:gwid/screens/manage_account_screen.dart'; import 'package:gwid/models/profile.dart'; import 'package:gwid/screens/settings/settings_screen.dart'; -import 'package:gwid/phone_entry_screen.dart'; +import 'package:gwid/screens/phone_entry_screen.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; class ProfileMenuDialog extends StatefulWidget { final Profile? myProfile; diff --git a/lib/screens/registration_screen.dart b/lib/screens/registration_screen.dart new file mode 100644 index 0000000..a113f61 --- /dev/null +++ b/lib/screens/registration_screen.dart @@ -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 createState() => _RegistrationScreenState(); +} + +class _RegistrationScreenState extends State + with TickerProviderStateMixin { + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _codeController = TextEditingController(); + + static const List _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 _topAlignmentAnimation; + late final Animation _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 _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 _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( + 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 countries; + final ValueChanged 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 countries; + final ValueChanged 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( + value: selectedCountry, + onChanged: onCountryChanged, + icon: Icon(Icons.keyboard_arrow_down, color: colors.onSurfaceVariant), + items: countries.map((Country country) { + return DropdownMenuItem( + 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(), + ), + ), + ); + } +} diff --git a/lib/search_channels_screen.dart b/lib/screens/search_channels_screen.dart similarity index 100% rename from lib/search_channels_screen.dart rename to lib/screens/search_channels_screen.dart diff --git a/lib/search_contact_screen.dart b/lib/screens/search_contact_screen.dart similarity index 73% rename from lib/search_contact_screen.dart rename to lib/screens/search_contact_screen.dart index 68902f6..39bfcd5 100644 --- a/lib/search_contact_screen.dart +++ b/lib/screens/search_contact_screen.dart @@ -1,9 +1,8 @@ - - import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/models/contact.dart'; +import 'package:gwid/screens/chat_screen.dart'; class SearchContactScreen extends StatefulWidget { const SearchContactScreen({super.key}); @@ -36,7 +35,6 @@ class _SearchContactScreenState extends State { _apiSubscription = ApiService.instance.messages.listen((message) { if (!mounted) return; - if (message['type'] == 'contact_found') { setState(() { _isLoading = false; @@ -48,22 +46,24 @@ class _SearchContactScreenState extends State { if (contactData != null) { _foundContact = Contact.fromJson(contactData); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Контакт найден!'), - backgroundColor: Colors.green, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + // Автоматически открываем чат с найденным контактом + _openChatWithContact(_foundContact!); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + 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') { setState(() { _isLoading = false; @@ -118,7 +118,6 @@ class _SearchContactScreenState extends State { return; } - if (!phone.startsWith('+') || phone.length < 10) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -160,20 +159,146 @@ class _SearchContactScreenState extends State { } } + Future _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?; + final myId = contactProfile?['id'] as int? ?? 0; + + if (myId == 0) { + print('⚠️ Не удалось получить myId, используем 0'); + } + + // Открываем ChatScreen + if (mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => ChatScreen( + chatId: chatId, + contact: contact, + myId: myId, + isGroupChat: false, + isChannel: false, + onChatUpdated: () { + print('Chat updated'); + }, + ), + ), + ); + } + } catch (e) { + print('❌ Ошибка при открытии чата: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ошибка при открытии чата: ${e.toString()}'), + backgroundColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(10), + ), + ); + } + } + } + void _startChat() { if (_foundContact != null) { + _openChatWithContact(_foundContact!); + } + } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Создание чата с ${_foundContact!.name}'), - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + Future _startChatAlternative() async { + if (_foundContact == null) return; + + try { + setState(() { + _isLoading = true; + }); + + print('🔄 Альтернативный способ: добавляем контакт ${_foundContact!.id}'); + + // Отправляем opcode=34 с action="ADD" + await ApiService.instance.addContact(_foundContact!.id); + print('✅ Отправлен opcode=34 с action=ADD'); + + // Отправляем opcode=35 с contactIds + await ApiService.instance.requestContactsByIds([_foundContact!.id]); + print('✅ Отправлен opcode=35 с contactIds=[${_foundContact!.id}]'); + + if (mounted) { + setState(() { + _isLoading = false; + }); + + // Показываем диалог о необходимости перезайти + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('Перезайти в приложение'), + content: const Text( + 'Для завершения добавления контакта необходимо перезайти в приложение.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Понятно'), + ), + ], ), - 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 { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -230,7 +354,6 @@ class _SearchContactScreenState extends State { const SizedBox(height: 24), - Text( 'Номер телефона', style: Theme.of( @@ -322,7 +445,6 @@ class _SearchContactScreenState extends State { ), ), - if (_foundContact != null) ...[ const SizedBox(height: 24), Container( @@ -411,12 +533,27 @@ class _SearchContactScreenState extends State { ), ), ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _startChatAlternative, + icon: const Icon(Icons.alternate_email), + label: const Text( + 'Начать чат альтернативным способом', + ), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), ], ), ), ], - if (_errorMessage != null) ...[ const SizedBox(height: 24), Container( diff --git a/lib/screens/settings/about_screen.dart b/lib/screens/settings/about_screen.dart index e7895f3..f9a29a7 100644 --- a/lib/screens/settings/about_screen.dart +++ b/lib/screens/settings/about_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:gwid/tos_screen.dart'; +import 'package:gwid/screens/tos_screen.dart'; +import 'package:gwid/consts.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutScreen extends StatelessWidget { @@ -239,7 +240,7 @@ class AboutScreen extends StatelessWidget { ), const SizedBox(height: 8), Text( - 'Версия 0.3.0', + 'Версия $version', style: TextStyle( fontSize: 16, color: colors.onSurface.withOpacity(0.7), diff --git a/lib/screens/settings/animations_screen.dart b/lib/screens/settings/animations_screen.dart index fedf5ff..286c039 100644 --- a/lib/screens/settings/animations_screen.dart +++ b/lib/screens/settings/animations_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; class AnimationsScreen extends StatelessWidget { diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index 238d0c0..be92d99 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:gwid/screens/settings/customization_screen.dart'; import 'package:gwid/screens/settings/animations_screen.dart'; diff --git a/lib/screens/settings/auth_settings_screen.dart b/lib/screens/settings/auth_settings_screen.dart index ccdf46b..e836ed1 100644 --- a/lib/screens/settings/auth_settings_screen.dart +++ b/lib/screens/settings/auth_settings_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:gwid/proxy_service.dart'; +import 'package:gwid/utils/proxy_service.dart'; import 'package:gwid/screens/settings/proxy_settings_screen.dart'; import 'package:gwid/screens/settings/session_spoofing_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/screens/settings/bypass_screen.dart b/lib/screens/settings/bypass_screen.dart index 6f786b9..7235884 100644 --- a/lib/screens/settings/bypass_screen.dart +++ b/lib/screens/settings/bypass_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; class BypassScreen extends StatelessWidget { final bool isModal; diff --git a/lib/screens/settings/customization_screen.dart b/lib/screens/settings/customization_screen.dart index 38c09eb..66e359b 100644 --- a/lib/screens/settings/customization_screen.dart +++ b/lib/screens/settings/customization_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'dart:io'; import 'dart:ui'; import 'package:gwid/models/message.dart'; diff --git a/lib/screens/settings/export_session_screen.dart b/lib/screens/settings/export_session_screen.dart index a6fd35b..5706706 100644 --- a/lib/screens/settings/export_session_screen.dart +++ b/lib/screens/settings/export_session_screen.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/proxy_service.dart'; -import 'package:gwid/spoofing_service.dart'; +import 'package:gwid/utils/proxy_service.dart'; +import 'package:gwid/utils/spoofing_service.dart'; import 'package:encrypt/encrypt.dart' as encrypt; import 'package:crypto/crypto.dart' as crypto; diff --git a/lib/screens/settings/privacy_settings_screen.dart b/lib/screens/settings/privacy_settings_screen.dart index 215e69b..f79be74 100644 --- a/lib/screens/settings/privacy_settings_screen.dart +++ b/lib/screens/settings/privacy_settings_screen.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:gwid/password_management_screen.dart'; +import 'package:gwid/screens/password_management_screen.dart'; class PrivacySettingsScreen extends StatefulWidget { const PrivacySettingsScreen({super.key}); diff --git a/lib/screens/settings/proxy_settings_screen.dart b/lib/screens/settings/proxy_settings_screen.dart index 12ec0fc..04ee2e7 100644 --- a/lib/screens/settings/proxy_settings_screen.dart +++ b/lib/screens/settings/proxy_settings_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:gwid/proxy_service.dart'; -import 'package:gwid/proxy_settings.dart'; +import 'package:gwid/utils/proxy_service.dart'; +import 'package:gwid/utils/proxy_settings.dart'; class ProxySettingsScreen extends StatefulWidget { const ProxySettingsScreen({super.key}); diff --git a/lib/screens/settings/reconnection_screen.dart b/lib/screens/settings/reconnection_screen.dart index ecefa1f..57a08e5 100644 --- a/lib/screens/settings/reconnection_screen.dart +++ b/lib/screens/settings/reconnection_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/home_screen.dart'; +import 'package:gwid/screens/home_screen.dart'; class ReconnectionScreen extends StatefulWidget { const ReconnectionScreen({super.key}); diff --git a/lib/screens/settings/session_spoofing_screen.dart b/lib/screens/settings/session_spoofing_screen.dart index dee313c..b726fef 100644 --- a/lib/screens/settings/session_spoofing_screen.dart +++ b/lib/screens/settings/session_spoofing_screen.dart @@ -8,7 +8,7 @@ import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:gwid/api/api_service.dart'; import 'package:uuid/uuid.dart'; -import 'package:gwid/device_presets.dart'; +import 'package:gwid/utils/device_presets.dart'; enum SpoofingMethod { partial, full } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index c25450c..6911a6f 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:gwid/consts.dart'; import 'package:gwid/models/profile.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/manage_account_screen.dart'; +import 'package:gwid/screens/manage_account_screen.dart'; import 'package:gwid/screens/settings/appearance_settings_screen.dart'; import 'package:gwid/screens/settings/notification_settings_screen.dart'; import 'package:gwid/screens/settings/privacy_security_screen.dart'; @@ -9,9 +10,9 @@ import 'package:gwid/screens/settings/storage_screen.dart'; import 'package:gwid/screens/settings/network_settings_screen.dart'; import 'package:gwid/screens/settings/bypass_screen.dart'; import 'package:gwid/screens/settings/about_screen.dart'; -import 'package:gwid/debug_screen.dart'; +import 'package:gwid/screens/debug_screen.dart'; import 'package:gwid/screens/settings/komet_misc_screen.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:provider/provider.dart'; class SettingsScreen extends StatefulWidget { @@ -208,7 +209,9 @@ class _SettingsScreenState extends State { ) : null, ), - body: _buildSettingsContent(), + body: SafeArea( + child: _buildSettingsContent(), + ), ); } @@ -468,7 +471,7 @@ class _SettingsScreenState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 24.0), child: Text( - 'v0.3.0-beta.1', + version, textAlign: TextAlign.center, style: TextStyle( color: Theme.of( diff --git a/lib/token_auth_screen.dart b/lib/screens/token_auth_screen.dart similarity index 98% rename from lib/token_auth_screen.dart rename to lib/screens/token_auth_screen.dart index c5805d1..a2c4a13 100644 --- a/lib/token_auth_screen.dart +++ b/lib/screens/token_auth_screen.dart @@ -11,9 +11,9 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:gwid/api/api_service.dart'; -import 'package:gwid/home_screen.dart'; -import 'package:gwid/proxy_service.dart'; -import 'package:gwid/proxy_settings.dart'; +import 'package:gwid/screens/home_screen.dart'; +import 'package:gwid/utils/proxy_service.dart'; +import 'package:gwid/utils/proxy_settings.dart'; import 'package:gwid/screens/settings/qr_scanner_screen.dart'; import 'package:gwid/screens/settings/session_spoofing_screen.dart'; diff --git a/lib/tos_screen.dart b/lib/screens/tos_screen.dart similarity index 100% rename from lib/tos_screen.dart rename to lib/screens/tos_screen.dart diff --git a/lib/device_presets.dart b/lib/utils/device_presets.dart similarity index 100% rename from lib/device_presets.dart rename to lib/utils/device_presets.dart diff --git a/lib/full_screen_video_player.dart b/lib/utils/full_screen_video_player.dart similarity index 100% rename from lib/full_screen_video_player.dart rename to lib/utils/full_screen_video_player.dart diff --git a/lib/image_cache_service.dart b/lib/utils/image_cache_service.dart similarity index 100% rename from lib/image_cache_service.dart rename to lib/utils/image_cache_service.dart diff --git a/lib/packet_framer.dart b/lib/utils/packet_framer.dart similarity index 100% rename from lib/packet_framer.dart rename to lib/utils/packet_framer.dart diff --git a/lib/proxy_service.dart b/lib/utils/proxy_service.dart similarity index 100% rename from lib/proxy_service.dart rename to lib/utils/proxy_service.dart diff --git a/lib/proxy_settings.dart b/lib/utils/proxy_settings.dart similarity index 100% rename from lib/proxy_settings.dart rename to lib/utils/proxy_settings.dart diff --git a/lib/spoofing_service.dart b/lib/utils/spoofing_service.dart similarity index 100% rename from lib/spoofing_service.dart rename to lib/utils/spoofing_service.dart diff --git a/lib/theme_provider.dart b/lib/utils/theme_provider.dart similarity index 100% rename from lib/theme_provider.dart rename to lib/utils/theme_provider.dart diff --git a/lib/universal_io.dart b/lib/utils/universal_io.dart similarity index 100% rename from lib/universal_io.dart rename to lib/utils/universal_io.dart diff --git a/lib/user_id_lookup_screen.dart b/lib/utils/user_id_lookup_screen.dart similarity index 100% rename from lib/user_id_lookup_screen.dart rename to lib/utils/user_id_lookup_screen.dart diff --git a/lib/widgets/chat_message_bubble.dart b/lib/widgets/chat_message_bubble.dart index 3f1d4e7..dbd68e2 100644 --- a/lib/widgets/chat_message_bubble.dart +++ b/lib/widgets/chat_message_bubble.dart @@ -9,26 +9,30 @@ import 'package:crypto/crypto.dart' as crypto; import 'package:intl/intl.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/models/contact.dart'; -import 'package:gwid/theme_provider.dart'; +import 'package:gwid/utils/theme_provider.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; import 'dart:ui'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:gwid/chat_screen.dart'; +import 'package:gwid/screens/chat_screen.dart'; import 'package:gwid/services/avatar_cache_service.dart'; import 'package:gwid/api/api_service.dart'; import 'dart:async'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:open_file/open_file.dart'; -import 'package:gwid/full_screen_video_player.dart'; +import 'package:gwid/widgets/full_screen_video_player.dart'; import 'package:just_audio/just_audio.dart'; import 'package:gwid/services/cache_service.dart'; import 'package:video_player/video_player.dart'; import 'package:gwid/services/music_player_service.dart'; +import 'package:platform_info/platform_info.dart'; bool _currentIsDark = false; +bool isMobile = Platform.instance.operatingSystem.iOS || + Platform.instance.operatingSystem.android; + enum MessageReadStatus { sending, // Отправляется (часы) sent, // Отправлено (1 галочка) @@ -1081,12 +1085,22 @@ class ChatMessageBubble extends StatelessWidget { ); if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { - messageContent = GestureDetector( + if (isMobile) { + messageContent = GestureDetector( onTapDown: (TapDownDetails details) { _showMessageContextMenu(context, details.globalPosition); }, child: messageContent, ); + } else { + messageContent = GestureDetector( + onSecondaryTapDown: (TapDownDetails details) { + _showMessageContextMenu(context, details.globalPosition); + }, + child: messageContent, + ); + } + } return Column( @@ -1535,12 +1549,21 @@ class ChatMessageBubble extends StatelessWidget { ); if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { - videoContent = GestureDetector( + if (isMobile) { + videoContent = GestureDetector( onTapDown: (TapDownDetails details) { _showMessageContextMenu(context, details.globalPosition); }, child: videoContent, ); + } else { + videoContent = GestureDetector( + onSecondaryTapDown: (TapDownDetails details) { + _showMessageContextMenu(context, details.globalPosition); + }, + child: videoContent, + ); + } } return videoContent; @@ -1621,12 +1644,21 @@ class ChatMessageBubble extends StatelessWidget { ); if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { - photoContent = GestureDetector( + if (isMobile) { + photoContent = GestureDetector( onTapDown: (TapDownDetails details) { _showMessageContextMenu(context, details.globalPosition); }, child: photoContent, ); + } else { + photoContent = GestureDetector( + onTapDown: (TapDownDetails details) { + _showMessageContextMenu(context, details.globalPosition); + }, + child: photoContent, + ); + } } return photoContent; @@ -1749,12 +1781,21 @@ class ChatMessageBubble extends StatelessWidget { ); if (onReaction != null || (isMe && (onEdit != null || onDelete != null))) { - videoContent = GestureDetector( + if (isMobile) { + videoContent = GestureDetector( onTapDown: (TapDownDetails details) { _showMessageContextMenu(context, details.globalPosition); }, child: videoContent, ); + } else { + videoContent = GestureDetector( + onSecondaryTapDown: (TapDownDetails details) { + _showMessageContextMenu(context, details.globalPosition); + }, + child: videoContent, + ); + } } return videoContent; diff --git a/lib/widgets/full_screen_video_player.dart b/lib/widgets/full_screen_video_player.dart new file mode 100644 index 0000000..3280d14 --- /dev/null +++ b/lib/widgets/full_screen_video_player.dart @@ -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 createState() => _FullScreenVideoPlayerState(); +} + +class _FullScreenVideoPlayerState extends State + 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 _controlsAnimation; + bool _isDragging = false; + Duration _currentPosition = Duration.zero; + Duration _totalDuration = Duration.zero; + List _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 _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 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 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, + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/message_preview_dialog.dart b/lib/widgets/message_preview_dialog.dart index 56cb796..47758d9 100644 --- a/lib/widgets/message_preview_dialog.dart +++ b/lib/widgets/message_preview_dialog.dart @@ -7,7 +7,7 @@ import 'package:gwid/models/profile.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/widgets/chat_message_bubble.dart'; import 'package:gwid/widgets/contact_name_widget.dart'; -import 'package:gwid/chat_screen.dart'; +import 'package:gwid/screens/chat_screen.dart'; class ControlMessageChip extends StatelessWidget { final Message message; diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -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 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 325ea1b..8adfbe3 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 3BE50CEC3B857AFB79ED4B51 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 388CA5C37E59612ABAEE6B9C /* Pods_RunnerTests.framework */; }; + CDF433146D1871A7EB701871 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89C4DA26E1D86992C17E0203 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1513A8E85A871669A708EFD4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 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 = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +80,14 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3BE50CEC3B857AFB79ED4B51 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CDF433146D1871A7EB701871 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + C9FFC0A80D79F5C5380DF8BD /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 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 = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 89C4DA26E1D86992C17E0203 /* Pods_Runner.framework */, + 388CA5C37E59612ABAEE6B9C /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + FBA29210F50B351DCEE6280B /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + FBA311D2EFFD2A674FD24C1D /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 2DB171D51BA2A164FD6822FA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -291,6 +323,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2DB171D51BA2A164FD6822FA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -329,6 +378,50 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + FBA29210F50B351DCEE6280B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FBA311D2EFFD2A674FD24C1D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 6C72571057485AEF9EBFBCC3 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1C6F713FF81D9CB5A449F94D /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8028056045BFBF1AE7C02DD1 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a3..cff5a4b 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,9 @@ com.apple.security.network.server + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..5ca243d 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + Komet CFBundlePackageType APPL CFBundleShortVersionString diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..38da9a9 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,9 @@ com.apple.security.app-sandbox + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 12c9db7..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,1551 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - app_links: - dependency: "direct main" - description: - name: app_links - sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.dev" - source: hosted - version: "1.6.5" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - audio_session: - dependency: transitive - description: - name: audio_session - sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" - url: "https://pub.dev" - source: hosted - version: "0.1.25" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca" - url: "https://pub.dev" - source: hosted - version: "1.13.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.dev" - source: hosted - version: "0.3.4+2" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - dbus: - dependency: transitive - description: - name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" - url: "https://pub.dev" - source: hosted - version: "0.7.11" - device_info_plus: - dependency: "direct main" - description: - name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.dev" - source: hosted - version: "10.1.2" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" - source: hosted - version: "7.0.3" - disable_battery_optimization: - dependency: "direct main" - description: - name: disable_battery_optimization - sha256: "7fbda76cdd30d01d75e091db8869b8cf2fd4c7b7edef3abce0d7206569603051" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - dynamic_color: - dependency: "direct main" - description: - name: dynamic_color - sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" - url: "https://pub.dev" - source: hosted - version: "1.8.1" - encrypt: - dependency: "direct main" - description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.dev" - source: hosted - version: "5.0.3" - equatable: - dependency: transitive - description: - name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" - source: hosted - version: "2.0.7" - es_compression: - dependency: "direct main" - description: - name: es_compression - sha256: c1ff7af54802631cf5c3942cb67bb99daadcc087f573ca99a9de91002d1a7ece - url: "https://pub.dev" - source: hosted - version: "2.0.15" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f - url: "https://pub.dev" - source: hosted - version: "10.3.3" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.dev" - source: hosted - version: "0.9.3+2" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" - url: "https://pub.dev" - source: hosted - version: "0.9.4+4" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" - source: hosted - version: "2.6.2" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" - url: "https://pub.dev" - source: hosted - version: "0.9.3+4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_background_service: - dependency: "direct main" - description: - name: flutter_background_service - sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d" - url: "https://pub.dev" - source: hosted - version: "5.1.0" - flutter_background_service_android: - dependency: transitive - description: - name: flutter_background_service_android - sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87 - url: "https://pub.dev" - source: hosted - version: "6.3.1" - flutter_background_service_ios: - dependency: transitive - description: - name: flutter_background_service_ios - sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba" - url: "https://pub.dev" - source: hosted - version: "5.0.3" - flutter_background_service_platform_interface: - dependency: transitive - description: - name: flutter_background_service_platform_interface - sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41 - url: "https://pub.dev" - source: hosted - version: "5.1.2" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_colorpicker: - dependency: "direct main" - description: - name: flutter_colorpicker - sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - flutter_highlight: - dependency: "direct main" - description: - name: flutter_highlight - sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - flutter_inappwebview: - dependency: "direct main" - description: - name: flutter_inappwebview - sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" - url: "https://pub.dev" - source: hosted - version: "6.1.5" - flutter_inappwebview_android: - dependency: transitive - description: - name: flutter_inappwebview_android - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.dev" - source: hosted - version: "1.1.3" - flutter_inappwebview_internal_annotations: - dependency: transitive - description: - name: flutter_inappwebview_internal_annotations - sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - flutter_inappwebview_ios: - dependency: transitive - description: - name: flutter_inappwebview_ios - sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_inappwebview_macos: - dependency: transitive - description: - name: flutter_inappwebview_macos - sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_inappwebview_platform_interface: - dependency: transitive - description: - name: flutter_inappwebview_platform_interface - sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 - url: "https://pub.dev" - source: hosted - version: "1.3.0+1" - flutter_inappwebview_web: - dependency: transitive - description: - name: flutter_inappwebview_web - sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_inappwebview_windows: - dependency: transitive - description: - name: flutter_inappwebview_windows - sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" - url: "https://pub.dev" - source: hosted - version: "0.6.0" - flutter_launcher_icons: - dependency: "direct main" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" - source: hosted - version: "0.14.4" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_markdown: - dependency: "direct main" - description: - name: flutter_markdown - sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" - url: "https://pub.dev" - source: hosted - version: "0.7.7+1" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 - url: "https://pub.dev" - source: hosted - version: "2.0.30" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" - url: "https://pub.dev" - source: hosted - version: "9.2.4" - flutter_secure_storage_linux: - dependency: transitive - description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" - source: hosted - version: "3.1.3" - flutter_secure_storage_platform_interface: - dependency: transitive - description: - name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_secure_storage_web: - dependency: transitive - description: - name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_windows: - dependency: transitive - description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_timezone: - dependency: "direct main" - description: - name: flutter_timezone - sha256: ccad42fbb5d01d51d3eb281cc4428fca556cc4063c52bd9fa40f80cd93b8e649 - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c" - url: "https://pub.dev" - source: hosted - version: "6.3.2" - gtk: - dependency: transitive - description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" - source: hosted - version: "2.1.0" - highlight: - dependency: transitive - description: - name: highlight - sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" - http: - dependency: "direct main" - description: - name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 - url: "https://pub.dev" - source: hosted - version: "1.5.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.dev" - source: hosted - version: "4.5.4" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02" - url: "https://pub.dev" - source: hosted - version: "0.8.13+3" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e - url: "https://pub.dev" - source: hosted - version: "0.8.13" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 - url: "https://pub.dev" - source: hosted - version: "0.2.2" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" - source: hosted - version: "0.2.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - just_audio: - dependency: "direct main" - description: - name: just_audio - sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e - url: "https://pub.dev" - source: hosted - version: "0.9.46" - just_audio_platform_interface: - dependency: transitive - description: - name: just_audio_platform_interface - sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" - url: "https://pub.dev" - source: hosted - version: "4.6.0" - just_audio_web: - dependency: transitive - description: - name: just_audio_web - sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" - url: "https://pub.dev" - source: hosted - version: "0.4.16" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - markdown: - dependency: transitive - description: - name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" - url: "https://pub.dev" - source: hosted - version: "7.3.0" - mask_text_input_formatter: - dependency: "direct main" - description: - name: mask_text_input_formatter - sha256: "978c58ec721c25621ceb468e633f4eef64b64d45424ac4540e0565d4f7c800cd" - url: "https://pub.dev" - source: hosted - version: "2.9.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: "5e7e09d904dc01de071b79b3f3789b302b0ed3c9c963109cd3f83ad90de62ecf" - url: "https://pub.dev" - source: hosted - version: "7.1.2" - msgpack_dart: - dependency: "direct main" - description: - name: msgpack_dart - sha256: c2d235ed01f364719b5296aecf43ac330f0d7bc865fa134d0d7910a40454dffb - url: "https://pub.dev" - source: hosted - version: "1.0.1" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - open_file: - dependency: "direct main" - description: - name: open_file - sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e - url: "https://pub.dev" - source: hosted - version: "3.5.10" - open_file_android: - dependency: transitive - description: - name: open_file_android - sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" - url: "https://pub.dev" - source: hosted - version: "1.0.6" - open_file_ios: - dependency: transitive - description: - name: open_file_ios - sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07" - url: "https://pub.dev" - source: hosted - version: "1.0.3" - open_file_linux: - dependency: transitive - description: - name: open_file_linux - sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 - url: "https://pub.dev" - source: hosted - version: "0.0.5" - open_file_mac: - dependency: transitive - description: - name: open_file_mac - sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" - url: "https://pub.dev" - source: hosted - version: "1.0.3" - open_file_platform_interface: - dependency: transitive - description: - name: open_file_platform_interface - sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" - url: "https://pub.dev" - source: hosted - version: "1.0.3" - open_file_web: - dependency: transitive - description: - name: open_file_web - sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f - url: "https://pub.dev" - source: hosted - version: "0.0.4" - open_file_windows: - dependency: transitive - description: - name: open_file_windows - sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 - url: "https://pub.dev" - source: hosted - version: "0.0.3" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.dev" - source: hosted - version: "8.3.1" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - path: - dependency: "direct main" - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" - url: "https://pub.dev" - source: hosted - version: "2.2.18" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" - url: "https://pub.dev" - source: hosted - version: "11.4.0" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc - url: "https://pub.dev" - source: hosted - version: "12.1.0" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.dev" - source: hosted - version: "9.4.7" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" - source: hosted - version: "0.1.3+5" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" - source: hosted - version: "4.3.0" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.dev" - source: hosted - version: "7.0.1" - pinput: - dependency: "direct main" - description: - name: pinput - sha256: "6d571e38a484f7515a52e89024ef416f11fa6171ac6f32303701374ab9890efa" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.dev" - source: hosted - version: "3.9.1" - posix: - dependency: transitive - description: - name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" - source: hosted - version: "6.0.3" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - qr_flutter: - dependency: "direct main" - description: - name: qr_flutter - sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - scrollable_positioned_list: - dependency: "direct main" - description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" - url: "https://pub.dev" - source: hosted - version: "0.3.8" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6" - url: "https://pub.dev" - source: hosted - version: "12.0.0" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f" - url: "https://pub.dev" - source: hosted - version: "2.4.14" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - smart_auth: - dependency: transitive - description: - name: smart_auth - sha256: "88aa8fe66e951c78a307f26d1c29672dce2e9eb3da2e12e853864d0e615a73ad" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - socks5_proxy: - dependency: "direct main" - description: - name: socks5_proxy - sha256: "80fa31a9ebfc0dc8de7b0e568c8d8927b65558ef2c7591cbee5afac814fb8f74" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - timezone: - dependency: "direct main" - description: - name: timezone - sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" - url: "https://pub.dev" - source: hosted - version: "0.9.4" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - universal_platform: - dependency: transitive - description: - name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b - url: "https://pub.dev" - source: hosted - version: "6.3.23" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 - url: "https://pub.dev" - source: hosted - version: "6.3.4" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f - url: "https://pub.dev" - source: hosted - version: "3.2.3" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" - url: "https://pub.dev" - source: hosted - version: "2.10.0" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: cf768d02924b91e333e2bc1ff928528f57d686445874f383bafab12d0bdfc340 - url: "https://pub.dev" - source: hosted - version: "2.8.17" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: "19ed1162a7a5520e7d7791e0b7b73ba03161b6a69428b82e4689e435b325432d" - url: "https://pub.dev" - source: hosted - version: "2.8.5" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: "9e372520573311055cb353b9a0da1c9d72b094b7ba01b8ecc66f28473553793b" - url: "https://pub.dev" - source: hosted - version: "6.5.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wakelock_plus: - dependency: transitive - description: - name: wakelock_plus - sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket_channel: - dependency: "direct main" - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - win32: - dependency: transitive - description: - name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" - url: "https://pub.dev" - source: hosted - version: "5.14.0" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" - url: "https://pub.dev" - source: hosted - version: "1.1.5" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.9.2 <4.0.0" - flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 14e6751..634a56b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -106,7 +106,9 @@ dependencies: 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 @@ -123,6 +125,7 @@ dependencies: chewie: ^1.7.5 just_audio: ^0.9.40 + platform_info: ^5.0.0 dev_dependencies: flutter_test: